From 01dfd24c9654ac935459c4f8e36e6598f055ad68 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Wed, 1 Mar 2023 23:09:45 +0000 Subject: [PATCH 001/154] Prepare Next Version --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index a9969ff5..04f75bc2 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -2,6 +2,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 VERSION_MINOR = 0 -VERSION_BUILD = 11 +VERSION_BUILD = 12 VERSION_ALPHA = 0 # END_VERSION_BLOCK From c575e24174dbfff8d97ebaa2dd3249b5ee242e95 Mon Sep 17 00:00:00 2001 From: emphasize Date: Thu, 2 Mar 2023 18:42:16 +0100 Subject: [PATCH 002/154] add `voc_list` helper function (#54) * add `voc_list` helper function * ovos public method --- ovos_workshop/skills/base.py | 31 ++++++++++++++------- ovos_workshop/skills/ovos.py | 23 ++++++++++----- test/unittests/skills/test_mycroft_skill.py | 9 ++++++ 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 371ae851..27a6e0f1 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -21,6 +21,7 @@ from dataclasses import dataclass from hashlib import md5 from inspect import signature +from typing import List from itertools import chain from os.path import join, abspath, dirname, basename, isfile from threading import Event @@ -133,7 +134,7 @@ def __init__(self, name=None, bus=None, resources_dir=None, self.reload_skill = True #: allow reloading (default True) self.events = EventContainer(bus) - self.voc_match_cache = {} + self._voc_cache = {} # loaded lang file resources self._lang_resources = {} @@ -1047,6 +1048,20 @@ def ask_selection(self, options, dialog='', resp = match return resp + # method not present in mycroft-core + def _voc_list(self, voc_filename, lang=None) -> List[str]: + + lang = lang or self.lang + cache_key = lang + voc_filename + + if cache_key not in self._voc_cache: + vocab = self._resources.load_vocabulary_file(voc_filename) or \ + CoreResources(lang).load_vocabulary_file(voc_filename) + if vocab: + self._voc_cache[cache_key] = list(chain(*vocab)) + + return self._voc_cache.get(cache_key) or [] + def voc_match(self, utt, voc_filename, lang=None, exact=False): """Determine if the given utterance contains the vocabulary provided. @@ -1070,21 +1085,17 @@ def voc_match(self, utt, voc_filename, lang=None, exact=False): bool: True if the utterance has the given vocabulary it """ match = False - lang = lang or self.lang - cache_key = lang + voc_filename - if cache_key not in self.voc_match_cache: - vocab = self._resources.load_vocabulary_file(voc_filename) or \ - CoreResources(lang).load_vocabulary_file(voc_filename) - self.voc_match_cache[cache_key] = list(chain(*vocab)) - if utt: + _vocs = self._voc_list(voc_filename, lang) + + if utt and _vocs: if exact: # Check for exact match match = any(i.strip() == utt - for i in self.voc_match_cache[cache_key]) + for i in _vocs) else: # Check for matches against complete words match = any([re.match(r'.*\b' + i + r'\b.*', utt) - for i in self.voc_match_cache[cache_key]]) + for i in _vocs]) return match diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index dfe68624..68e3dcbf 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -1,5 +1,6 @@ import re import time +from typing import List from ovos_utils.intents import IntentBuilder, Intent from ovos_utils.log import LOG @@ -132,17 +133,25 @@ def voc_match(self, *args, **kwargs): except FileNotFoundError: return False - def remove_voc(self, utt, voc_filename, lang=None): - """ removes any entry in .voc file from the utterance """ - lang = lang or self.lang - cache_key = lang + voc_filename + def voc_list(self, voc_filename, lang=None) -> List[str]: + """ + Get vocabulary list and cache the results + + Args: + voc_filename (str): Name of vocabulary file (e.g. 'yes' for + 'res/text/en-us/yes.voc') + lang (str): Language code, defaults to self.lang - if cache_key not in self.voc_match_cache: - self.voc_match(utt, voc_filename, lang) + Returns: + list: List of vocabulary found in voc_filename + """ + return self._voc_list(voc_filename, lang) + def remove_voc(self, utt, voc_filename, lang=None): + """ removes any entry in .voc file from the utterance """ if utt: # Check for matches against complete words - for i in self.voc_match_cache.get(cache_key) or []: + for i in self.voc_list(voc_filename, lang): # Substitute only whole words matching the token utt = re.sub(r'\b' + i + r"\b", "", utt) diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill.py index 53c7c76b..859e8f27 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill.py @@ -437,6 +437,15 @@ def test_voc_match_exact(self): exact=True)) self.assertFalse(s.voc_match("would you please turn off the lights", "turn_off_test", exact=True)) + + def test_voc_list(self): + s = SimpleSkill1() + s.root_dir = abspath(dirname(__file__)) + + self.assertEqual(s._voc_list("turn_off_test"), + ["turn off", "switch off"]) + cache_key = s.lang+"turn_off_test" + self.assertIn(cache_key, s._voc_cache) def test_translate_locations(self): """Assert that the a translatable list can be loaded from dialog and From e05d9aa6ce2a4980b4d573c84b873600468f2821 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 2 Mar 2023 17:43:18 +0000 Subject: [PATCH 003/154] Increment Version --- CHANGELOG.md | 16 ++++++++++++++++ ovos_workshop/version.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 443dd24b..f1b12a2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [Unreleased](https://github.com/OpenVoiceOS/OVOS-workshop/tree/HEAD) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11...HEAD) + +**Closed issues:** + +- Add helper function `voc_list` to the OvosSkill class [\#53](https://github.com/OpenVoiceOS/OVOS-workshop/issues/53) + +**Merged pull requests:** + +- add `voc_list` helper function [\#54](https://github.com/OpenVoiceOS/OVOS-workshop/pull/54) ([emphasize](https://github.com/emphasize)) + +## [V0.0.11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11) (2023-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a6...V0.0.11) + ## [V0.0.11a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a6) (2023-02-25) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a5...V0.0.11a6) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 04f75bc2..b87abf1b 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK From 47096d9367e8a24ca3a2db1842c9dddb2bba4c1b Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 2 Mar 2023 14:14:45 -0800 Subject: [PATCH 004/154] Add backwards-compat. voc_cache property and setter (#55) --- ovos_workshop/skills/base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 27a6e0f1..72f3b238 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -189,6 +189,21 @@ def network_requirements(self): "will be removed in ovos-core 0.0.8") return self.runtime_requirements + @property + def voc_match_cache(self): + """ + Backwards-compatible accessor method for vocab cache + @return: dict vocab resources to parsed resources + """ + return self._voc_cache + + @voc_match_cache.setter + def voc_match_cache(self, val): + LOG.warning("self._voc_cache should not be modified externally. This" + "functionality will be deprecated in a future release") + if isinstance(val, dict): + self._voc_cache = val + # property not present in mycroft-core @property def _is_fully_initialized(self): From 693a213a666bdc17e1c82809d4eff838a5e6f732 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 2 Mar 2023 22:15:57 +0000 Subject: [PATCH 005/154] Increment Version --- CHANGELOG.md | 10 +++++++++- ovos_workshop/version.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b12a2a..61c07e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased](https://github.com/OpenVoiceOS/OVOS-workshop/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a1...HEAD) **Closed issues:** @@ -10,6 +10,14 @@ **Merged pull requests:** +- Add backwards-compat. voc\_cache property and setter [\#55](https://github.com/OpenVoiceOS/OVOS-workshop/pull/55) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a1) (2023-03-02) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11...V0.0.12a1) + +**Merged pull requests:** + - add `voc_list` helper function [\#54](https://github.com/OpenVoiceOS/OVOS-workshop/pull/54) ([emphasize](https://github.com/emphasize)) ## [V0.0.11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11) (2023-02-25) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index b87abf1b..04ed9f95 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 1 +VERSION_ALPHA = 2 # END_VERSION_BLOCK From 50d2e917062753ca9cf2776c3fa669054e95cdbe Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 5 Apr 2023 04:32:42 +0100 Subject: [PATCH 006/154] ovos-bus-client + skillGui class in ovos_workshop (#57) * ovos-bus-client + skillGui class in ovos_workshop * bump ovos-utils --- ovos_workshop/skills/base.py | 86 +++++++++++++++++-- ovos_workshop/skills/common_play.py | 2 +- requirements/requirements.txt | 6 +- test/unittests/skills/test_mycroft_skill.py | 2 +- .../skills/test_mycroft_skill_get_response.py | 2 +- test/unittests/test_skill.py | 2 +- 6 files changed, 83 insertions(+), 17 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 72f3b238..468b1223 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -18,19 +18,17 @@ import time import traceback from copy import copy -from dataclasses import dataclass from hashlib import md5 from inspect import signature -from typing import List from itertools import chain from os.path import join, abspath, dirname, basename, isfile from threading import Event - +from typing import List from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list from lingua_franca.parse import yes_or_no, extract_number -from mycroft_bus_client.message import Message, dig_for_message +from ovos_bus_client.message import Message, dig_for_message from ovos_backend_client.api import EmailApi, MetricsApi from ovos_config.config import Configuration from ovos_config.locations import get_xdg_config_save_path @@ -46,6 +44,7 @@ from ovos_utils.log import LOG from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang from ovos_utils.parse import match_one +from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties from ovos_utils.sound import play_acknowledge_sound, wait_while_speaking @@ -56,7 +55,6 @@ from ovos_workshop.filesystem import FileSystemAccess from ovos_workshop.resource_files import ResourceFile, \ CoreResources, SkillResources, find_resource -from ovos_utils.process_utils import RuntimeRequirements # backwards compat alias @@ -67,6 +65,76 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +class SkillGUI(GUIInterface): + """SkillGUI - Interface to the Graphical User Interface + + Values set in this class are synced to the GUI, accessible within QML + via the built-in sessionData mechanism. For example, in Python you can + write in a skill: + self.gui['temp'] = 33 + self.gui.show_page('Weather.qml') + Then in the Weather.qml you'd access the temp via code such as: + text: sessionData.time + """ + + def __init__(self, skill): + self.skill = skill + super().__init__(skill.skill_id, config=Configuration()) + + @property + def bus(self): + if self.skill: + return self.skill.bus + + @property + def skill_id(self): + return self.skill.skill_id + + def setup_default_handlers(self): + """Sets the handlers for the default messages.""" + msg_type = self.build_message_type('set') + self.skill.add_event(msg_type, self.gui_set) + + def register_handler(self, event, handler): + """Register a handler for GUI events. + + When using the triggerEvent method from Qt + triggerEvent("event", {"data": "cool"}) + + Args: + event (str): event to catch + handler: function to handle the event + """ + msg_type = self.build_message_type(event) + self.skill.add_event(msg_type, handler) + + def _pages2uri(self, page_names): + # Convert pages to full reference + page_urls = [] + for name in page_names: + page = self.skill._resources.locate_qml_file(name) + if page: + if self.remote_url: + page_urls.append(self.remote_url + "/" + page) + elif page.startswith("file://"): + page_urls.append(page) + else: + page_urls.append("file://" + page) + else: + raise FileNotFoundError(f"Unable to find page: {name}") + + return page_urls + + def shutdown(self): + """Shutdown gui interface. + + Clear pages loaded through this interface and remove the skill + reference to make ref counting warning more precise. + """ + self.release() + self.skill = None + + def simple_trace(stack_trace): """Generate a simplified traceback. @@ -427,7 +495,7 @@ def bus(self): @bus.setter def bus(self, value): - from mycroft_bus_client import MessageBusClient + from ovos_bus_client import MessageBusClient from ovos_utils.messagebus import FakeBus if isinstance(value, (MessageBusClient, FakeBus)): self._bus = value @@ -622,7 +690,7 @@ def _stop_is_implemented(self): @property def _converse_is_implemented(self): return self.__class__.converse is not BaseSkill.converse or \ - self.__original_converse != self.converse + self.__original_converse != self.converse def _register_system_event_handlers(self): """Add all events allowing the standard interaction with the Mycroft @@ -1069,12 +1137,12 @@ def _voc_list(self, voc_filename, lang=None) -> List[str]: lang = lang or self.lang cache_key = lang + voc_filename - if cache_key not in self._voc_cache: + if cache_key not in self._voc_cache: vocab = self._resources.load_vocabulary_file(voc_filename) or \ CoreResources(lang).load_vocabulary_file(voc_filename) if vocab: self._voc_cache[cache_key] = list(chain(*vocab)) - + return self._voc_cache.get(cache_key) or [] def voc_match(self, utt, voc_filename, lang=None, exact=False): diff --git a/ovos_workshop/skills/common_play.py b/ovos_workshop/skills/common_play.py index 031d8300..fc5b4607 100644 --- a/ovos_workshop/skills/common_play.py +++ b/ovos_workshop/skills/common_play.py @@ -2,7 +2,7 @@ from threading import Event from ovos_workshop.decorators.ocp import * from ovos_workshop.skills.ovos import OVOSSkill, MycroftSkill -from mycroft_bus_client import Message +from ovos_bus_client import Message from ovos_utils.log import LOG diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 4b765447..77abadcd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,4 @@ -ovos-utils~=0.0, >=0.0.28 +ovos-utils[extras]~=0.0, >=0.0.31a3 ovos_config~=0.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 - -# optional but improves fuzzy matching and silences logs -rapidfuzz \ No newline at end of file +ovos-bus-client~=0.0, >=0.0.3a4 \ No newline at end of file diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill.py index 859e8f27..ba49bb9f 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill.py @@ -24,7 +24,7 @@ from adapt.intent import IntentBuilder from ovos_config.config import Configuration -from mycroft_bus_client import Message +from ovos_bus_client import Message from ovos_workshop.skills.mycroft_skill import MycroftSkill from .mocks import base_config diff --git a/test/unittests/skills/test_mycroft_skill_get_response.py b/test/unittests/skills/test_mycroft_skill_get_response.py index 2468a050..9924fca9 100644 --- a/test/unittests/skills/test_mycroft_skill_get_response.py +++ b/test/unittests/skills/test_mycroft_skill_get_response.py @@ -8,7 +8,7 @@ from lingua_franca import load_language from ovos_workshop.skills.mycroft_skill import MycroftSkill -from mycroft_bus_client import Message +from ovos_bus_client import Message from .mocks import base_config, AnyCallable diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index f0cccc7f..996ceb6d 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -2,7 +2,7 @@ import unittest from unittest.mock import Mock -from mycroft_bus_client import Message +from ovos_bus_client import Message from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core From 480603c292500a33f98e177ecac4770473bd5314 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 5 Apr 2023 03:33:51 +0000 Subject: [PATCH 007/154] Increment Version --- CHANGELOG.md | 10 +++++++++- ovos_workshop/version.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c07e48..5a5fc89d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ ## [Unreleased](https://github.com/OpenVoiceOS/OVOS-workshop/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a1...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a2...HEAD) + +**Merged pull requests:** + +- ovos-bus-client + skillGui class in ovos\_workshop [\#57](https://github.com/OpenVoiceOS/OVOS-workshop/pull/57) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a2) (2023-03-02) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a1...V0.0.12a2) **Closed issues:** diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 04ed9f95..3d22ba87 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 2 +VERSION_ALPHA = 3 # END_VERSION_BLOCK From 210c56820ba9679e63c2d141c108f935a83228c1 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 14 Apr 2023 05:22:28 +0100 Subject: [PATCH 008/154] feat/common_qa_class (#61) --- ovos_workshop/res/text/cs-cz/noise_words.list | 24 ++ ovos_workshop/res/text/de-de/noise_words.list | 52 +++++ ovos_workshop/res/text/en-us/noise_words.list | 30 +++ ovos_workshop/skills/common_query_skill.py | 215 ++++++++++++++++++ 4 files changed, 321 insertions(+) create mode 100644 ovos_workshop/res/text/cs-cz/noise_words.list create mode 100644 ovos_workshop/res/text/de-de/noise_words.list create mode 100644 ovos_workshop/res/text/en-us/noise_words.list create mode 100644 ovos_workshop/skills/common_query_skill.py diff --git a/ovos_workshop/res/text/cs-cz/noise_words.list b/ovos_workshop/res/text/cs-cz/noise_words.list new file mode 100644 index 00000000..903cc381 --- /dev/null +++ b/ovos_workshop/res/text/cs-cz/noise_words.list @@ -0,0 +1,24 @@ +kde +co je +který +jim +oni +kdy +co +to +bude +od +z +že +také +kdo +jak +a +ale +také +proč +pro +je +to +nebo +do diff --git a/ovos_workshop/res/text/de-de/noise_words.list b/ovos_workshop/res/text/de-de/noise_words.list new file mode 100644 index 00000000..cc619c9f --- /dev/null +++ b/ovos_workshop/res/text/de-de/noise_words.list @@ -0,0 +1,52 @@ +wo +wohin +sie +ihnen +sie +man +wann +als +wo +was +welcher +welche +welches +der +die +das +dass +daß +werden +werde +wirst +wird +werdet +wollen +willst +von +auch +wer +wie +tat +taten +und +aber +auch +warum +für +ist +es +tun +tut +oder +zu +auf +bis +von +aus +um +ein +einer +eines +mal +bitte diff --git a/ovos_workshop/res/text/en-us/noise_words.list b/ovos_workshop/res/text/en-us/noise_words.list new file mode 100644 index 00000000..85cf69de --- /dev/null +++ b/ovos_workshop/res/text/en-us/noise_words.list @@ -0,0 +1,30 @@ +where +what's +which +them +they +when +what +that +will +from +that +also +who +how +did +and +but +the +too +why +for +is +it +do +or +to +of +a + + diff --git a/ovos_workshop/skills/common_query_skill.py b/ovos_workshop/skills/common_query_skill.py new file mode 100644 index 00000000..d6860bb8 --- /dev/null +++ b/ovos_workshop/skills/common_query_skill.py @@ -0,0 +1,215 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import abstractmethod +from enum import IntEnum +from os.path import dirname + +from ovos_utils.file_utils import resolve_resource_file +from ovos_utils.log import LOG + +from ovos_workshop.skills.ovos import OVOSSkill + + +class CQSMatchLevel(IntEnum): + EXACT = 1 # Skill could find a specific answer for the question + CATEGORY = 2 # Skill could find an answer from a category in the query + GENERAL = 3 # The query could be processed as a general quer + + +# Copy of CQSMatchLevel to use if the skill returns visual media +CQSVisualMatchLevel = IntEnum('CQSVisualMatchLevel', + [e.name for e in CQSMatchLevel]) + +"""these are for the confidence calculation""" +# how much each topic word is worth +# when found in the answer +TOPIC_MATCH_RELEVANCE = 5 + +# elevate relevance above all else +RELEVANCE_MULTIPLIER = 2 + +# we like longer articles but only so much +MAX_ANSWER_LEN_FOR_CONFIDENCE = 50 + +# higher number - less bias for word length +WORD_COUNT_DIVISOR = 100 + + +class CommonQuerySkill(OVOSSkill): + """Question answering skills should be based on this class. + + The skill author needs to implement `CQS_match_query_phrase` returning an + answer and can optionally implement `CQS_action` to perform additional + actions if the skill's answer is selected. + + This class works in conjunction with skill-query which collects + answers from several skills presenting the best one available. + """ + + def __init__(self, name=None, bus=None): + super().__init__(name, bus) + noise_words_filepath = f"text/{self.lang}/noise_words.list" + default_res = f"{dirname(dirname(__file__))}/res/text/{self.lang}/noise_words.list" + noise_words_filename = resolve_resource_file(noise_words_filepath) or \ + resolve_resource_file(default_res) + self.translated_noise_words = [] + if noise_words_filename: + with open(noise_words_filename) as f: + self.translated_noise_words = f.read().strip() + self.translated_noise_words = self.translated_noise_words.split() + + # these should probably be configurable + self.level_confidence = { + CQSMatchLevel.EXACT: 0.9, + CQSMatchLevel.CATEGORY: 0.6, + CQSMatchLevel.GENERAL: 0.5 + } + + def bind(self, bus): + """Overrides the default bind method of MycroftSkill. + + This registers messagebus handlers for the skill during startup + but is nothing the skill author needs to consider. + """ + if bus: + super().bind(bus) + self.add_event('question:query', self.__handle_question_query, speak_errors=False) + self.add_event('question:action', self.__handle_query_action, speak_errors=False) + + def __handle_question_query(self, message): + search_phrase = message.data["phrase"] + message.context["skill_id"] = self.skill_id + # First, notify the requestor that we are attempting to handle + # (this extends a timeout while this skill looks for a match) + self.bus.emit(message.response({"phrase": search_phrase, + "skill_id": self.skill_id, + "searching": True})) + + # Now invoke the CQS handler to let the skill perform its search + try: + result = self.CQS_match_query_phrase(search_phrase) + except: + LOG.exception(f"error matching {search_phrase} with {self.skill_id}") + result = None + + if result: + match = result[0] + level = result[1] + answer = result[2] + callback = result[3] if len(result) > 3 else None + confidence = self.__calc_confidence( + match, search_phrase, level, answer) + self.bus.emit(message.response({"phrase": search_phrase, + "skill_id": self.skill_id, + "answer": answer, + "callback_data": callback, + "conf": confidence})) + else: + # Signal we are done (can't handle it) + self.bus.emit(message.response({"phrase": search_phrase, + "skill_id": self.skill_id, + "searching": False})) + + def remove_noise(self, phrase): + """remove noise to produce essence of question""" + phrase = ' ' + phrase + ' ' + for word in self.translated_noise_words: + mtch = ' ' + word + ' ' + if phrase.find(mtch) > -1: + phrase = phrase.replace(mtch, " ") + phrase = ' '.join(phrase.split()) + return phrase.strip() + + def __calc_confidence(self, match, phrase, level, answer): + # Assume the more of the words that get consumed, the better the match + consumed_pct = len(match.split()) / len(phrase.split()) + if consumed_pct > 1.0: + consumed_pct = 1.0 + consumed_pct /= 10 + + # bonus for more sentences + num_sentences = float(float(len(answer.split("."))) / float(10)) + + # extract topic + topic = self.remove_noise(match) + + # calculate relevance + answer = answer.lower() + matches = 0 + for word in topic.split(' '): + if answer.find(word) > -1: + matches += TOPIC_MATCH_RELEVANCE + + answer_size = len(answer.split(" ")) + answer_size = min(MAX_ANSWER_LEN_FOR_CONFIDENCE, answer_size) + + relevance = 0.0 + if answer_size > 0: + relevance = float(float(matches) / float(answer_size)) + + relevance = relevance * RELEVANCE_MULTIPLIER + + # extra credit for more words up to a point + wc_mod = float(float(answer_size) / float(WORD_COUNT_DIVISOR)) * 2 + + confidence = self.level_confidence[level] + \ + consumed_pct + num_sentences + relevance + wc_mod + + return confidence + + def __handle_query_action(self, message): + """Message handler for question:action. + + Extracts phrase and data from message forward this to the skills + CQS_action method. + """ + if message.data["skill_id"] != self.skill_id: + # Not for this skill! + return + phrase = message.data["phrase"] + data = message.data.get("callback_data") + # Invoke derived class to provide playback data + self.CQS_action(phrase, data) + + @abstractmethod + def CQS_match_query_phrase(self, phrase): + """Analyze phrase to see if it is a play-able phrase with this skill. + + Needs to be implemented by the skill. + + Args: + phrase (str): User phrase, "What is an aardwark" + + Returns: + (match, CQSMatchLevel[, callback_data]) or None: Tuple containing + a string with the appropriate matching phrase, the PlayMatch + type, and optionally data to return in the callback if the + match is selected. + """ + # Derived classes must implement this, e.g. + return None + + def CQS_action(self, phrase, data): + """Take additional action IF the skill is selected. + + The speech is handled by the common query but if the chosen skill + wants to display media, set a context or prepare for sending + information info over e-mail this can be implemented here. + + Args: + phrase (str): User phrase uttered after "Play", e.g. "some music" + data (dict): Callback data specified in match_query_phrase() + """ + # Derived classes may implement this if they use additional media + # or wish to set context after being called. + return None From 59de1cd5f713ab06e2f8cbfeb77af2211a37503f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 14 Apr 2023 04:23:26 +0000 Subject: [PATCH 009/154] Increment Version --- CHANGELOG.md | 10 +++++++++- ovos_workshop/version.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5fc89d..2c8a4116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,15 @@ ## [Unreleased](https://github.com/OpenVoiceOS/OVOS-workshop/tree/HEAD) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a2...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a3...HEAD) + +**Implemented enhancements:** + +- feat/common\_qa\_class [\#61](https://github.com/OpenVoiceOS/OVOS-workshop/pull/61) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a3) (2023-04-05) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a2...V0.0.12a3) **Merged pull requests:** diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 3d22ba87..7d25f89b 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 3 +VERSION_ALPHA = 4 # END_VERSION_BLOCK From 6b9e374ef8cc3cf90fed5958c3446062d6b011df Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:04:15 -0700 Subject: [PATCH 010/154] Update automation to match ovos-utils (#62) --- .github/workflows/propose_release.yml | 32 ++++++++++++++ .github/workflows/publish_alpha.yml | 60 +++++++++++---------------- .github/workflows/publish_release.yml | 43 +++++++++++++++++++ 3 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/propose_release.yml create mode 100644 .github/workflows/publish_release.yml diff --git a/.github/workflows/propose_release.yml b/.github/workflows/propose_release.yml new file mode 100644 index 00000000..72db0763 --- /dev/null +++ b/.github/workflows/propose_release.yml @@ -0,0 +1,32 @@ +name: Propose Stable Release +on: + workflow_dispatch: + inputs: + release_type: + type: choice + description: Release Type + options: + - build + - minor + - major +jobs: + update_version: + uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + with: + release_type: ${{ inputs.release_type }} + version_file: ovos_workshop/version.py + alpha_var: VERSION_ALPHA + build_var: VERSION_BUILD + minor_var: VERSION_MINOR + major_var: VERSION_MAJOR + update_changelog: True + branch: dev + + pull_changes: + needs: update_version + uses: neongeckocom/.github/.github/workflows/pull_master.yml@master + with: + pr_assignee: ${{ github.actor }} + pr_draft: false + pr_title: ${{ needs.update_version.outputs.version }} + pr_body: ${{ needs.update_version.outputs.changelog }} diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index 27779cc5..ad6f76cc 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -19,54 +19,44 @@ on: workflow_dispatch: jobs: + update_version: + uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + with: + release_type: "alpha" + version_file: ovos_workshop/version.py + alpha_var: VERSION_ALPHA + build_var: VERSION_BUILD + minor_var: VERSION_MINOR + major_var: VERSION_MAJOR + update_changelog: True + branch: dev build_and_publish: runs-on: ubuntu-latest + needs: update_version steps: - - uses: actions/checkout@v2 - with: - ref: dev - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Increment Version - run: | - VER=$(python setup.py --version) - python scripts/bump_alpha.py - - name: "Generate release changelog" - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: changelog - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Increment Version - branch: dev - - name: version - run: echo "::set-output name=version::$(python setup.py --version)" - id: version - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: - tag_name: V${{ steps.version.outputs.version }} - release_name: Release ${{ steps.version.outputs.version }} + tag_name: V${{ needs.update_version.outputs.version }} + release_name: Release ${{ needs.update_version.outputs.version }} body: | Changes in this Release - ${{ steps.changelog.outputs.changelog }} + ${{ needs.update_version.outputs.changelog }} draft: false prerelease: true + commitish: dev + - name: Checkout Repository + uses: actions/checkout@v2 + with: + ref: dev + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - name: Build Distribution Packages run: | - python setup.py bdist_wheel - - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@master + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file + password: ${{secrets.PYPI_TOKEN}} diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml new file mode 100644 index 00000000..edb91cdc --- /dev/null +++ b/.github/workflows/publish_release.yml @@ -0,0 +1,43 @@ +name: Publish Release +on: + push: + branches: + - master + +jobs: + github_release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: master + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. + - name: version + run: echo "::set-output name=version::$(python setup.py --version)" + id: version + - name: "Generate release changelog" + uses: heinrichreimer/github-changelog-generator-action@v2.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + id: changelog + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: V${{ steps.version.outputs.version }} + release_name: Release ${{ steps.version.outputs.version }} + body: | + Changes in this Release + ${{ steps.changelog.outputs.changelog }} + draft: false + prerelease: false + commitish: master + - name: Build Distribution Packages + run: | + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file From b96ea7e956fe2fe044823f43e9a1fe7a027f5eae Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 18 Apr 2023 21:07:35 +0000 Subject: [PATCH 011/154] Increment Version to 0.0.12a5 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 7d25f89b..ca16b7fa 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 4 +VERSION_ALPHA = 5 # END_VERSION_BLOCK From a2f754ca39bda8143e716965586826b389a641a1 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Tue, 18 Apr 2023 21:08:12 +0000 Subject: [PATCH 012/154] Update Changelog --- CHANGELOG.md | 302 ++------------------------------------------------- 1 file changed, 10 insertions(+), 292 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8a4116..f4475dbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [Unreleased](https://github.com/OpenVoiceOS/OVOS-workshop/tree/HEAD) +## [0.0.12a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a5) (2023-04-18) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a3...HEAD) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a4...0.0.12a5) + +**Merged pull requests:** + +- Update automation to match ovos-utils [\#62](https://github.com/OpenVoiceOS/OVOS-workshop/pull/62) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a4) (2023-04-14) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a3...V0.0.12a4) **Implemented enhancements:** @@ -36,296 +44,6 @@ - add `voc_list` helper function [\#54](https://github.com/OpenVoiceOS/OVOS-workshop/pull/54) ([emphasize](https://github.com/emphasize)) -## [V0.0.11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11) (2023-02-25) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a6...V0.0.11) - -## [V0.0.11a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a6) (2023-02-25) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a5...V0.0.11a6) - -**Merged pull requests:** - -- Update ovos\_utils dependency to stable release [\#52](https://github.com/OpenVoiceOS/OVOS-workshop/pull/52) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.11a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a5) (2023-02-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a4...V0.0.11a5) - -**Fixed bugs:** - -- allow skills to bump workshop and still work in core \< 0.0.7 [\#51](https://github.com/OpenVoiceOS/OVOS-workshop/pull/51) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.11a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a4) (2023-02-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a3...V0.0.11a4) - -**Merged pull requests:** - -- feat/generalize runtime requirements [\#49](https://github.com/OpenVoiceOS/OVOS-workshop/pull/49) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.11a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a3) (2023-02-02) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a2...V0.0.11a3) - -**Fixed bugs:** - -- replace is\_ovos with is\_classic\_core [\#48](https://github.com/OpenVoiceOS/OVOS-workshop/pull/48) ([AIIX](https://github.com/AIIX)) - -## [V0.0.11a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a2) (2023-01-30) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a1...V0.0.11a2) - -## [V0.0.11a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a1) (2023-01-28) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10...V0.0.11a1) - -**Fixed bugs:** - -- allow empty string value settings [\#47](https://github.com/OpenVoiceOS/OVOS-workshop/pull/47) ([emphasize](https://github.com/emphasize)) - -## [V0.0.10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10) (2023-01-24) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a6...V0.0.10) - -**Merged pull requests:** - -- Update ovos-core ref in unit tests [\#46](https://github.com/OpenVoiceOS/OVOS-workshop/pull/46) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.10a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a6) (2023-01-24) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a5...V0.0.10a6) - -**Merged pull requests:** - -- Update dependencies to stable versions [\#45](https://github.com/OpenVoiceOS/OVOS-workshop/pull/45) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.10a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a5) (2023-01-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a4...V0.0.10a5) - -**Fixed bugs:** - -- Fix message context handling in `__handle_stop` [\#44](https://github.com/OpenVoiceOS/OVOS-workshop/pull/44) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.10a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a4) (2023-01-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a3...V0.0.10a4) - -**Merged pull requests:** - -- Deprecate Application-managed `settings` [\#43](https://github.com/OpenVoiceOS/OVOS-workshop/pull/43) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.10a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a3) (2023-01-18) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a2...V0.0.10a3) - -**Merged pull requests:** - -- refactor the old patches module [\#34](https://github.com/OpenVoiceOS/OVOS-workshop/pull/34) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.10a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a2) (2023-01-17) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a1...V0.0.10a2) - -**Implemented enhancements:** - -- Get response from validated value [\#35](https://github.com/OpenVoiceOS/OVOS-workshop/pull/35) ([emphasize](https://github.com/emphasize)) - -## [V0.0.10a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a1) (2022-12-15) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.9...V0.0.10a1) - -**Implemented enhancements:** - -- feat/SkillNetworkRequirements [\#36](https://github.com/OpenVoiceOS/OVOS-workshop/pull/36) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.9) (2022-10-20) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.9a1...V0.0.9) - -## [V0.0.9a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.9a1) (2022-10-20) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8...V0.0.9a1) - -**Fixed bugs:** - -- Fix circular imports [\#32](https://github.com/OpenVoiceOS/OVOS-workshop/pull/32) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a6...V0.0.8) - -## [V0.0.8a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a6) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a5...V0.0.8a6) - -**Merged pull requests:** - -- shared code with ovos utils [\#31](https://github.com/OpenVoiceOS/OVOS-workshop/pull/31) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.8a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a5) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a4...V0.0.8a5) - -## [V0.0.8a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a4) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a3...V0.0.8a4) - -**Implemented enhancements:** - -- feat/resource\_utils [\#30](https://github.com/OpenVoiceOS/OVOS-workshop/pull/30) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.8a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a3) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a2...V0.0.8a3) - -**Implemented enhancements:** - -- improve send\_stop\_signal [\#29](https://github.com/OpenVoiceOS/OVOS-workshop/pull/29) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.8a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a2) (2022-10-19) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a1...V0.0.8a2) - -**Merged pull requests:** - -- move OCP to optional requirements [\#28](https://github.com/OpenVoiceOS/OVOS-workshop/pull/28) ([JarbasAl](https://github.com/JarbasAl)) -- license + vulnerability tests [\#27](https://github.com/OpenVoiceOS/OVOS-workshop/pull/27) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.8a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a1) (2022-09-13) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7...V0.0.8a1) - -**Merged pull requests:** - -- document results structure in ocp\_search [\#26](https://github.com/OpenVoiceOS/OVOS-workshop/pull/26) ([NeonDaniel](https://github.com/NeonDaniel)) -- feat/mycroft\_integration\_tests [\#25](https://github.com/OpenVoiceOS/OVOS-workshop/pull/25) ([NeonJarbas](https://github.com/NeonJarbas)) - -## [V0.0.7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7) (2022-07-29) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a12...V0.0.7) - -## [V0.0.7a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a12) (2022-07-29) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a11...V0.0.7a12) - -**Merged pull requests:** - -- add initial tests for killable\_events [\#24](https://github.com/OpenVoiceOS/OVOS-workshop/pull/24) ([NeonJarbas](https://github.com/NeonJarbas)) - -## [V0.0.7a11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a11) (2022-07-28) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a10...V0.0.7a11) - -**Implemented enhancements:** - -- feat/play\_audio [\#23](https://github.com/OpenVoiceOS/OVOS-workshop/pull/23) ([NeonJarbas](https://github.com/NeonJarbas)) - -## [V0.0.7a10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a10) (2022-07-22) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a9...V0.0.7a10) - -**Merged pull requests:** - -- Update dependencies and config references [\#22](https://github.com/OpenVoiceOS/OVOS-workshop/pull/22) ([NeonDaniel](https://github.com/NeonDaniel)) - -## [V0.0.7a9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a9) (2022-06-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a8...V0.0.7a9) - -## [V0.0.7a8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a8) (2022-06-02) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a7...V0.0.7a8) - -**Fixed bugs:** - -- Fix/decouple from mycroft [\#21](https://github.com/OpenVoiceOS/OVOS-workshop/pull/21) ([NeonJarbas](https://github.com/NeonJarbas)) - -## [V0.0.7a7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a7) (2022-05-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a6...V0.0.7a7) - -## [V0.0.7a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a6) (2022-05-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a5...V0.0.7a6) - -## [V0.0.7a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a5) (2022-05-09) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a4...V0.0.7a5) - -## [V0.0.7a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a4) (2022-05-07) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a3...V0.0.7a4) - -## [V0.0.7a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a3) (2022-05-07) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a2...V0.0.7a3) - -## [V0.0.7a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a2) (2022-05-07) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a1...V0.0.7a2) - -**Merged pull requests:** - -- Fix/optional adapt [\#20](https://github.com/OpenVoiceOS/OVOS-workshop/pull/20) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.7a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a1) (2022-05-07) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6...V0.0.7a1) - -**Fixed bugs:** - -- Fix/optional adapt [\#19](https://github.com/OpenVoiceOS/OVOS-workshop/pull/19) ([JarbasAl](https://github.com/JarbasAl)) - -## [V0.0.6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.6) (2022-03-03) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6a1...V0.0.6) - -## [V0.0.6a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.6a1) (2022-03-03) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5...V0.0.6a1) - -**Breaking changes:** - -- remove/skillgui\_patches [\#18](https://github.com/OpenVoiceOS/OVOS-workshop/pull/18) ([JarbasAl](https://github.com/JarbasAl)) - -**Closed issues:** - -- OVOSSkill class inherited skills do not initialize [\#17](https://github.com/OpenVoiceOS/OVOS-workshop/issues/17) - -## [V0.0.5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5) (2022-02-25) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5a12...V0.0.5) - -## [V0.0.5a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5a12) (2022-02-25) - -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/d9261b124f73a3e4d50c6edfcd9c2243b2bc3cf6...V0.0.5a12) - -**Implemented enhancements:** - -- add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) -- Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) -- add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) -- Add new work in progress audio player ui for media service [\#6](https://github.com/OpenVoiceOS/OVOS-workshop/pull/6) ([AIIX](https://github.com/AIIX)) -- Add next and previous buttons [\#4](https://github.com/OpenVoiceOS/OVOS-workshop/pull/4) ([AIIX](https://github.com/AIIX)) -- add player pos property and fix mycroft players for plugin [\#3](https://github.com/OpenVoiceOS/OVOS-workshop/pull/3) ([AIIX](https://github.com/AIIX)) - -**Fixed bugs:** - -- Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) -- remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) -- fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) -- fix icon paths and lower version [\#7](https://github.com/OpenVoiceOS/OVOS-workshop/pull/7) ([AIIX](https://github.com/AIIX)) -- fix AudioPlayer property name [\#5](https://github.com/OpenVoiceOS/OVOS-workshop/pull/5) ([AIIX](https://github.com/AIIX)) -- fix condition in video player [\#2](https://github.com/OpenVoiceOS/OVOS-workshop/pull/2) ([AIIX](https://github.com/AIIX)) -- add a timeout to videoplayer when nothing is playing for more than 60 seconds [\#1](https://github.com/OpenVoiceOS/OVOS-workshop/pull/1) ([AIIX](https://github.com/AIIX)) - -**Merged pull requests:** - -- Feat/workflows [\#16](https://github.com/OpenVoiceOS/OVOS-workshop/pull/16) ([JarbasAl](https://github.com/JarbasAl)) -- feat/pypi\_automation [\#13](https://github.com/OpenVoiceOS/OVOS-workshop/pull/13) ([JarbasAl](https://github.com/JarbasAl)) - \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From ec8a882940703d25fd0c644b66271f6c02f5051f Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 19 Apr 2023 17:29:28 -0700 Subject: [PATCH 013/154] Cleanup Changes (#64) --- .github/workflows/publish_build.yml | 76 ----------------------------- .github/workflows/publish_major.yml | 76 ----------------------------- .github/workflows/publish_minor.yml | 76 ----------------------------- requirements/requirements.txt | 4 +- scripts/bump_alpha.py | 18 ------- scripts/bump_build.py | 21 -------- scripts/bump_major.py | 27 ---------- scripts/bump_minor.py | 24 --------- scripts/remove_alpha.py | 13 ----- 9 files changed, 2 insertions(+), 333 deletions(-) delete mode 100644 .github/workflows/publish_build.yml delete mode 100644 .github/workflows/publish_major.yml delete mode 100644 .github/workflows/publish_minor.yml delete mode 100644 scripts/bump_alpha.py delete mode 100644 scripts/bump_build.py delete mode 100644 scripts/bump_major.py delete mode 100644 scripts/bump_minor.py delete mode 100644 scripts/remove_alpha.py diff --git a/.github/workflows/publish_build.yml b/.github/workflows/publish_build.yml deleted file mode 100644 index 9fdcd309..00000000 --- a/.github/workflows/publish_build.yml +++ /dev/null @@ -1,76 +0,0 @@ -# This workflow will generate a distribution and upload it to PyPI - -name: Publish Build Release ..X -on: - workflow_dispatch: - -jobs: - build_and_publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: dev - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Remove alpha (declare stable) - run: | - VER=$(python setup.py --version) - python scripts/remove_alpha.py - - name: "Generate release changelog" - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: changelog - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Declare alpha stable - branch: dev - - name: Push dev -> master - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: master - force: true - - name: version - run: echo "::set-output name=version::$(python setup.py --version)" - id: version - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ steps.version.outputs.version }} - release_name: Release ${{ steps.version.outputs.version }} - body: | - Changes in this Release - ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: false - - name: Build Distribution Packages - run: | - python setup.py bdist_wheel - - name: Prepare next Build version - run: echo "::set-output name=version::$(python setup.py --version)" - id: alpha - - name: Increment Version ${{ steps.alpha.outputs.version }}Alpha0 - run: | - VER=$(python setup.py --version) - python scripts/bump_build.py - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Prepare Next Version - branch: dev - - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/publish_major.yml b/.github/workflows/publish_major.yml deleted file mode 100644 index 87cee864..00000000 --- a/.github/workflows/publish_major.yml +++ /dev/null @@ -1,76 +0,0 @@ -# This workflow will generate a distribution and upload it to PyPI - -name: Publish Major Release X.0.0 -on: - workflow_dispatch: - -jobs: - build_and_publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: dev - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Remove alpha (declare stable) - run: | - VER=$(python setup.py --version) - python scripts/remove_alpha.py - - name: "Generate release changelog" - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: changelog - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Declare alpha stable - branch: dev - - name: Push dev -> master - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: master - force: true - - name: version - run: echo "::set-output name=version::$(python setup.py --version)" - id: version - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ steps.version.outputs.version }} - release_name: Release ${{ steps.version.outputs.version }} - body: | - Changes in this Release - ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: false - - name: Build Distribution Packages - run: | - python setup.py bdist_wheel - - name: Prepare next Major version - run: echo "::set-output name=version::$(python setup.py --version)" - id: alpha - - name: Increment Version ${{ steps.alpha.outputs.version }}Alpha0 - run: | - VER=$(python setup.py --version) - python scripts/bump_major.py - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Prepare Next Version - branch: dev - - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/publish_minor.yml b/.github/workflows/publish_minor.yml deleted file mode 100644 index 4e8b2312..00000000 --- a/.github/workflows/publish_minor.yml +++ /dev/null @@ -1,76 +0,0 @@ -# This workflow will generate a distribution and upload it to PyPI - -name: Publish Minor Release .X.0 -on: - workflow_dispatch: - -jobs: - build_and_publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: dev - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Remove alpha (declare stable) - run: | - VER=$(python setup.py --version) - python scripts/remove_alpha.py - - name: "Generate release changelog" - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: changelog - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Declare alpha stable - branch: dev - - name: Push dev -> master - uses: ad-m/github-push-action@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: master - force: true - - name: version - run: echo "::set-output name=version::$(python setup.py --version)" - id: version - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ steps.version.outputs.version }} - release_name: Release ${{ steps.version.outputs.version }} - body: | - Changes in this Release - ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: false - - name: Build Distribution Packages - run: | - python setup.py bdist_wheel - - name: Prepare next Minor version - run: echo "::set-output name=version::$(python setup.py --version)" - id: alpha - - name: Increment Version ${{ steps.alpha.outputs.version }}Alpha0 - run: | - VER=$(python setup.py --version) - python scripts/bump_minor.py - - name: Commit to dev - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: Prepare Next Version - branch: dev - - name: Publish to Test PyPI - uses: pypa/gh-action-pypi-publish@master - with: - password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 77abadcd..c01def64 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos-utils[extras]~=0.0, >=0.0.31a3 +ovos-utils[extras] < 0.1.0, >=0.0.31 ovos_config~=0.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 -ovos-bus-client~=0.0, >=0.0.3a4 \ No newline at end of file +ovos-bus-client < 0.1.0, >=0.0.3 \ No newline at end of file diff --git a/scripts/bump_alpha.py b/scripts/bump_alpha.py deleted file mode 100644 index 39a81aa4..00000000 --- a/scripts/bump_alpha.py +++ /dev/null @@ -1,18 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "ovos_workshop", "version.py") -version_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_build.py b/scripts/bump_build.py deleted file mode 100644 index 5704edf9..00000000 --- a/scripts/bump_build.py +++ /dev/null @@ -1,21 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "ovos_workshop", "version.py") -version_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_major.py b/scripts/bump_major.py deleted file mode 100644 index 20b86609..00000000 --- a/scripts/bump_major.py +++ /dev/null @@ -1,27 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "ovos_workshop", "version.py") -version_var_name = "VERSION_MAJOR" -minor_var_name = "VERSION_MINOR" -build_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(minor_var_name): - print(f"{minor_var_name} = 0") - elif line.startswith(build_var_name): - print(f"{build_var_name} = 0") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/bump_minor.py b/scripts/bump_minor.py deleted file mode 100644 index a293b4a1..00000000 --- a/scripts/bump_minor.py +++ /dev/null @@ -1,24 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "ovos_workshop", "version.py") -version_var_name = "VERSION_MINOR" -build_var_name = "VERSION_BUILD" -alpha_var_name = "VERSION_ALPHA" - -with open(version_file, "r", encoding="utf-8") as v: - for line in v.readlines(): - if line.startswith(version_var_name): - version = int(line.split("=")[-1]) - new_version = int(version) + 1 - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(version_var_name): - print(f"{version_var_name} = {new_version}") - elif line.startswith(build_var_name): - print(f"{build_var_name} = 0") - elif line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) diff --git a/scripts/remove_alpha.py b/scripts/remove_alpha.py deleted file mode 100644 index d7d055f9..00000000 --- a/scripts/remove_alpha.py +++ /dev/null @@ -1,13 +0,0 @@ -import fileinput -from os.path import join, dirname - - -version_file = join(dirname(dirname(__file__)), "ovos_workshop", "version.py") - -alpha_var_name = "VERSION_ALPHA" - -for line in fileinput.input(version_file, inplace=True): - if line.startswith(alpha_var_name): - print(f"{alpha_var_name} = 0") - else: - print(line.rstrip('\n')) From a051d31a3f68b6c43068882bf767da35614b22bb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 20 Apr 2023 00:29:46 +0000 Subject: [PATCH 014/154] Increment Version to 0.0.12a6 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index ca16b7fa..550dae6c 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 5 +VERSION_ALPHA = 6 # END_VERSION_BLOCK From e8d71db2a91f0b55b0a7d13a68b65c9a8a66ba4e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 20 Apr 2023 00:30:13 +0000 Subject: [PATCH 015/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4475dbb..b17d092e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a5) (2023-04-18) +## [0.0.12a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a6) (2023-04-20) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a4...0.0.12a5) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a5...0.0.12a6) + +**Merged pull requests:** + +- Cleanup Changes [\#64](https://github.com/OpenVoiceOS/OVOS-workshop/pull/64) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a5) (2023-04-18) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a4...V0.0.12a5) **Merged pull requests:** From 347f237b6396aee1cf9f9376077a3680141b4dd2 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Thu, 20 Apr 2023 03:25:25 +0100 Subject: [PATCH 016/154] feat/skill_launcher (#65) --- ovos_workshop/skill_launcher.py | 533 ++++++++++++++++++++++++++++++++ ovos_workshop/skills/base.py | 88 +++--- setup.py | 7 +- 3 files changed, 587 insertions(+), 41 deletions(-) create mode 100644 ovos_workshop/skill_launcher.py diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py new file mode 100644 index 00000000..a64e5da1 --- /dev/null +++ b/ovos_workshop/skill_launcher.py @@ -0,0 +1,533 @@ +import gc +import importlib +import os +from os.path import isdir +import sys +from inspect import isclass +from os import path, makedirs +from time import time + +from ovos_bus_client.client import MessageBusClient +from ovos_bus_client.message import Message +from ovos_config.config import Configuration +from ovos_config.locations import get_xdg_data_dirs, get_xdg_data_save_path +from ovos_plugin_manager.skills import find_skill_plugins +from ovos_utils import wait_for_exit_signal +from ovos_utils.file_utils import FileWatcher +from ovos_utils.log import LOG +from ovos_utils.process_utils import RuntimeRequirements + +from ovos_workshop.skills.active import ActiveSkill +from ovos_workshop.skills.auto_translatable import UniversalSkill, UniversalFallback +from ovos_workshop.skills.base import BaseSkill +from ovos_workshop.skills.common_play import OVOSCommonPlaybackSkill +from ovos_workshop.skills.common_query_skill import CommonQuerySkill +from ovos_workshop.skills.fallback import FallbackSkill +from ovos_workshop.skills.mycroft_skill import MycroftSkill +from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill + +SKILL_BASE_CLASSES = [ + BaseSkill, MycroftSkill, OVOSSkill, OVOSFallbackSkill, + OVOSCommonPlaybackSkill, OVOSFallbackSkill, CommonQuerySkill, ActiveSkill, + FallbackSkill, UniversalSkill, UniversalFallback +] + +SKILL_MAIN_MODULE = '__init__.py' + + +def get_skill_directories(conf=None): + """ returns list of skill directories ordered by expected loading order + + This corresponds to: + - XDG_DATA_DIRS + - user defined extra directories + + Each directory contains individual skill folders to be loaded + + If a skill exists in more than one directory (same folder name) previous instances will be ignored + ie. directories at the end of the list have priority over earlier directories + + NOTE: empty folders are interpreted as disabled skills + + new directories can be defined in mycroft.conf by specifying a full path + each extra directory is expected to contain individual skill folders to be loaded + + the xdg folder name can also be changed, it defaults to "skills" + eg. ~/.local/share/mycroft/FOLDER_NAME + + { + "skills": { + "directory": "skills", + "extra_directories": ["path/to/extra/dir/to/scan/for/skills"] + } + } + + Args: + conf (dict): mycroft.conf dict, will be loaded automatically if None + """ + # the contents of each skills directory must be individual skill folders + # we are still dependent on the mycroft-core structure of skill_id/__init__.py + + conf = conf or Configuration() + folder = conf["skills"].get("directory") + + # load all valid XDG paths + # NOTE: skills are actually code, but treated as user data! + # they should be considered applets rather than full applications + skill_locations = list(reversed( + [os.path.join(p, folder) for p in get_xdg_data_dirs()] + )) + + # load additional explicitly configured directories + conf = conf.get("skills") or {} + # extra_directories is a list of directories containing skill subdirectories + # NOT a list of individual skill folders + skill_locations += conf.get("extra_directories") or [] + return skill_locations + + +def get_default_skills_directory(): + """ return default directory to scan for skills + + data_dir is always XDG_DATA_DIR + If xdg is disabled then data_dir by default corresponds to /opt/mycroft + + users can define the data directory in mycroft.conf + the skills folder name (relative to data_dir) can also be defined there + + NOTE: folder name also impacts all XDG skill directories! + + { + "skills": { + "directory_override": "/opt/mycroft/hardcoded_path/skills" + } + } + + Args: + conf (dict): mycroft.conf dict, will be loaded automatically if None + """ + folder = Configuration()["skills"].get("directory") + skills_folder = os.path.join(get_xdg_data_save_path(), folder) + # create folder if needed + makedirs(skills_folder, exist_ok=True) + return path.expanduser(skills_folder) + + +def remove_submodule_refs(module_name): + """Ensure submodules are reloaded by removing the refs from sys.modules. + + Python import system puts a reference for each module in the sys.modules + dictionary to bypass loading if a module is already in memory. To make + sure skills are completely reloaded these references are deleted. + + Args: + module_name: name of skill module. + """ + submodules = [] + LOG.debug(f'Skill module: {module_name}') + # Collect found submodules + for m in sys.modules: + if m.startswith(module_name + '.'): + submodules.append(m) + # Remove all references them to in sys.modules + for m in submodules: + LOG.debug(f'Removing sys.modules ref for {m}') + del sys.modules[m] + + +def load_skill_module(path, skill_id): + """Load a skill module + + This function handles the differences between python 3.4 and 3.5+ as well + as makes sure the module is inserted into the sys.modules dict. + + Args: + path: Path to the skill main file (__init__.py) + skill_id: skill_id used as skill identifier in the module list + """ + module_name = skill_id.replace('.', '_') + + remove_submodule_refs(module_name) + + spec = importlib.util.spec_from_file_location(module_name, path) + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) + return mod + + +def get_skill_class(skill_module): + """Find MycroftSkill based class in skill module. + + Arguments: + skill_module (module): module to search for Skill class + + Returns: + (MycroftSkill): Found subclass of MycroftSkill or None. + """ + if callable(skill_module): + # it's a skill plugin + # either a func that returns the skill or the skill class itself + return skill_module + + candidates = [] + for name, obj in skill_module.__dict__.items(): + if isclass(obj): + if any(issubclass(obj, c) for c in SKILL_BASE_CLASSES) and \ + not any(obj is c for c in SKILL_BASE_CLASSES): + candidates.append(obj) + + for candidate in list(candidates): + others = [clazz for clazz in candidates if clazz != candidate] + # if we found a subclass of this candidate, it is not the final skill + if any(issubclass(clazz, candidate) for clazz in others): + candidates.remove(candidate) + + if candidates: + if len(candidates) > 1: + LOG.warning(f"Multiple skills found in a single file!\n" + f"{candidates}") + LOG.debug(f"Loading skill class: {candidates[0]}") + return candidates[0] + return None + + +def get_create_skill_function(skill_module): + """Find create_skill function in skill module. + + Arguments: + skill_module (module): module to search for create_skill function + + Returns: + (function): Found create_skill function or None. + """ + if hasattr(skill_module, "create_skill") and \ + callable(skill_module.create_skill): + return skill_module.create_skill + return None + + +class SkillLoader: + def __init__(self, bus, skill_directory=None, skill_id=None): + self.bus = bus + self._skill_directory = skill_directory + self._skill_id = skill_id + self._skill_class = None + self._loaded = None + self.load_attempted = False + self.last_loaded = 0 + self.instance: BaseSkill = None + self.active = True + self._watchdog = None + self.config = Configuration() + self.skill_module = None + + @property + def loaded(self): + return self._loaded # or self.instance is None + + @loaded.setter + def loaded(self, val): + self._loaded = val + + @property + def skill_directory(self): + skill_dir = self._skill_directory + if self.instance and not skill_dir: + skill_dir = self.instance.root_dir + return skill_dir + + @skill_directory.setter + def skill_directory(self, val): + self._skill_directory = val + + @property + def skill_id(self): + skill_id = self._skill_id + if self.instance and not skill_id: + skill_id = self.instance.skill_id + if self.skill_directory and not skill_id: + skill_id = os.path.basename(self.skill_directory) + return skill_id + + @skill_id.setter + def skill_id(self, val): + self._skill_id = val + + @property + def skill_class(self): + skill_class = self._skill_class + if self.instance and not skill_class: + skill_class = self.instance.__class__ + if self.skill_module and not skill_class: + skill_class = get_skill_class(self.skill_module) + return skill_class + + @skill_class.setter + def skill_class(self, val): + self._skill_class = val + + @property + def runtime_requirements(self): + if not self.skill_class: + return RuntimeRequirements() + return self.skill_class.runtime_requirements + + @property + def is_blacklisted(self): + """Boolean value representing whether or not a skill is blacklisted.""" + blacklist = self.config['skills'].get('blacklisted_skills') or [] + if self.skill_id in blacklist: + return True + else: + return False + + @property + def reload_allowed(self): + return self.active and (self.instance is None or self.instance.reload_skill) + + def reload(self): + LOG.info(f'ATTEMPTING TO RELOAD SKILL: {self.skill_id}') + if self.instance: + if not self.instance.reload_skill: + LOG.info("skill does not allow reloading!") + return False # not allowed + self._unload() + return self._load() + + def load(self): + LOG.info(f'ATTEMPTING TO LOAD SKILL: {self.skill_id}') + return self._load() + + def _unload(self): + """Remove listeners and stop threads before loading""" + if self._watchdog: + self._watchdog.shutdown() + self._watchdog = None + + self._execute_instance_shutdown() + if self.config.get("debug", False): + self._garbage_collect() + self._emit_skill_shutdown_event() + + def unload(self): + if self.instance: + self._execute_instance_shutdown() + + def activate(self): + self.active = True + self.load() + + def deactivate(self): + self.active = False + self.unload() + + def _execute_instance_shutdown(self): + """Call the shutdown method of the skill being reloaded.""" + try: + self.instance.default_shutdown() + except Exception: + LOG.exception(f'An error occurred while shutting down {self.skill_id}') + else: + LOG.info(f'Skill {self.skill_id} shut down successfully') + del self.instance + self.instance = None + + def _garbage_collect(self): + """Invoke Python garbage collector to remove false references""" + gc.collect() + # Remove two local references that are known + refs = sys.getrefcount(self.instance) - 2 + if refs > 0: + LOG.warning( + f"After shutdown of {self.skill_id} there are still {refs} references " + "remaining. The skill won't be cleaned from memory." + ) + + def _emit_skill_shutdown_event(self): + message = Message("mycroft.skills.shutdown", + {"path": self.skill_directory, "id": self.skill_id}) + self.bus.emit(message) + + def _load(self): + self._prepare_for_load() + if self.is_blacklisted: + self._skip_load() + else: + self.skill_module = self._load_skill_source() + self.loaded = self._create_skill_instance() + + self.last_loaded = time() + self._communicate_load_status() + self._start_filewatcher() + return self.loaded + + def _start_filewatcher(self): + if not self._watchdog: + self._watchdog = FileWatcher([self.skill_directory], + callback=self._handle_filechange, + recursive=True) + + def _handle_filechange(self): + LOG.info("Skill change detected!") + try: + if self.reload_allowed: + self.reload() + except Exception: + LOG.exception(f'Unhandled exception occurred while reloading {self.skill_directory}') + + def _prepare_for_load(self): + self.load_attempted = True + self.instance = None + + def _skip_load(self): + LOG.info(f'Skill {self.skill_id} is blacklisted - it will not be loaded') + + def _load_skill_source(self): + """Use Python's import library to load a skill's source code.""" + main_file_path = os.path.join(self.skill_directory, SKILL_MAIN_MODULE) + skill_module = None + if not os.path.exists(main_file_path): + LOG.error(f'Failed to load {self.skill_id} due to a missing file.') + else: + try: + skill_module = load_skill_module(main_file_path, self.skill_id) + except Exception as e: + LOG.exception(f'Failed to load skill: {self.skill_id} ({e})') + return skill_module + + def _create_skill_instance(self, skill_module=None): + """create the skill object. + + Arguments: + skill_module (module): Module to load from + + Returns: + (bool): True if skill was loaded successfully. + """ + skill_module = skill_module or self.skill_module + try: + skill_creator = get_create_skill_function(skill_module) or \ + self.skill_class + + # create the skill + # if the signature supports skill_id and bus pass them + # to fully initialize the skill in 1 go + try: + # many skills do not expose this, if they don't allow bus/skill_id kwargs + # in __init__ we need to manually call _startup + self.instance = skill_creator(bus=self.bus, + skill_id=self.skill_id) + # skills will have bus and skill_id available as soon as they call super() + except: + self.instance = skill_creator() + + if hasattr(self.instance, "is_fully_initialized"): + LOG.warning(f"Deprecated skill signature! Skill class should be" + f" imported from `ovos_workshop.skills`") + is_initialized = self.instance.is_fully_initialized + else: + is_initialized = self.instance._is_fully_initialized + if not is_initialized: + # finish initialization of skill class + self.instance._startup(self.bus, self.skill_id) + except Exception as e: + LOG.exception(f'Skill __init__ failed with {e}') + self.instance = None + + return self.instance is not None + + def _communicate_load_status(self): + if self.loaded: + message = Message('mycroft.skills.loaded', + {"path": self.skill_directory, + "id": self.skill_id, + "name": self.instance.name}) + self.bus.emit(message) + LOG.info(f'Skill {self.skill_id} loaded successfully') + else: + message = Message('mycroft.skills.loading_failure', + {"path": self.skill_directory, "id": self.skill_id}) + self.bus.emit(message) + if not self.is_blacklisted: + LOG.error(f'Skill {self.skill_id} failed to load') + else: + LOG.info(f'Skill {self.skill_id} not loaded') + + +class PluginSkillLoader(SkillLoader): + def __init__(self, bus, skill_id): + super().__init__(bus, skill_id=skill_id) + + def load(self, skill_class): + LOG.info('ATTEMPTING TO LOAD PLUGIN SKILL: ' + self.skill_id) + self._skill_class = skill_class + self._prepare_for_load() + if self.is_blacklisted: + self._skip_load() + else: + self.loaded = self._create_skill_instance() + + self.last_loaded = time() + self._communicate_load_status() + return self.loaded + + +def launch_plugin_skill(skill_id): + """ run a plugin skill standalone """ + bus = MessageBusClient() + bus.run_in_thread() + plugins = find_skill_plugins() + if skill_id not in plugins: + raise ValueError(f"unknown skill_id: {skill_id}") + skill_plugin = plugins[skill_id] + skill_loader = PluginSkillLoader(bus, skill_id) + try: + skill_loader.load(skill_plugin) + wait_for_exit_signal() + except KeyboardInterrupt: + skill_loader.deactivate() + except Exception: + LOG.exception(f'Load of skill {skill_id} failed!') + + +def launch_standalone_skill(skill_directory, skill_id): + """ run a skill standalone from a directory """ + bus = MessageBusClient() + bus.run_in_thread() + skill_loader = SkillLoader(bus, skill_directory, + skill_id=skill_id) + try: + skill_loader.load() + wait_for_exit_signal() + except KeyboardInterrupt: + skill_loader.deactivate() + except Exception: + LOG.exception(f'Load of skill {skill_directory} failed!') + + +def _launch_script(): + """USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]""" + if (args_count := len(sys.argv)) == 2: + skill_id = sys.argv[1] + + # preference to local skills + for p in get_skill_directories(): + if isdir(f"{p}/{skill_id}"): + skill_directory = f"{p}/{skill_id}" + LOG.info(f"found local skill, loading {skill_directory}") + launch_standalone_skill(skill_directory, skill_id) + break + else: # plugin skill + LOG.info(f"found plugin skill {skill_id}") + launch_plugin_skill(skill_id) + + elif args_count == 3: + # user asked explicitly for a directory + skill_id = sys.argv[1] + skill_directory = sys.argv[2] + launch_standalone_skill(skill_directory, skill_id) + else: + print("USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]") + raise SystemExit(2) + diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 468b1223..64b8326e 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -28,8 +28,8 @@ from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list from lingua_franca.parse import yes_or_no, extract_number -from ovos_bus_client.message import Message, dig_for_message from ovos_backend_client.api import EmailApi, MetricsApi +from ovos_bus_client.message import Message, dig_for_message from ovos_config.config import Configuration from ovos_config.locations import get_xdg_config_save_path from ovos_utils import camel_case_split @@ -162,13 +162,15 @@ class BaseSkill: def __init__(self, name=None, bus=None, resources_dir=None, settings: JsonStorage = None, - gui=None, enable_settings_manager=True): + gui=None, enable_settings_manager=True, + skill_id=""): + self.log = LOG # a dedicated namespace will be assigned in _startup self._enable_settings_manager = enable_settings_manager self._init_event = Event() self.name = name or self.__class__.__name__ self.resting_name = None - self.skill_id = '' # will be set by SkillLoader, guaranteed unique + self.skill_id = skill_id # will be set by SkillLoader, guaranteed unique self._settings_meta = None # DEPRECATED - backwards compat only self.settings_manager = None @@ -216,6 +218,10 @@ def __init__(self, name=None, bus=None, resources_dir=None, self.__original_converse = self.converse + # yay, following python best practices again! + if self.skill_id and self.bus: + self._startup(self.bus, self.skill_id) + # classproperty not present in mycroft-core @classproperty def runtime_requirements(self): @@ -267,8 +273,8 @@ def voc_match_cache(self): @voc_match_cache.setter def voc_match_cache(self, val): - LOG.warning("self._voc_cache should not be modified externally. This" - "functionality will be deprecated in a future release") + self.log.warning("self._voc_cache should not be modified externally. This" + "functionality will be deprecated in a future release") if isinstance(val, dict): self._voc_cache = val @@ -294,7 +300,7 @@ def _check_for_first_run(self): """Determine if its the very first time a skill is run.""" first_run = self.settings.get("__mycroft_skill_firstrun", True) if first_run: - LOG.info("First run of " + self.skill_id) + self.log.info("First run of " + self.skill_id) self._handle_first_run() self.settings["__mycroft_skill_firstrun"] = False self.settings.store() @@ -312,7 +318,7 @@ def _startup(self, bus, skill_id=""): but skill loader can override this """ if self._is_fully_initialized: - LOG.warning(f"Tried to initialize {self.skill_id} multiple times, ignoring") + self.log.warning(f"Tried to initialize {self.skill_id} multiple times, ignoring") return # NOTE: this method is called by SkillLoader @@ -343,7 +349,7 @@ def _startup(self, bus, skill_id=""): self._check_for_first_run() self._init_event.set() except Exception as e: - LOG.exception('Skill initialization failed') + self.log.exception('Skill initialization failed') # If an exception occurs, attempt to clean up the skill try: self.default_shutdown() @@ -353,15 +359,14 @@ def _startup(self, bus, skill_id=""): def _init_settings(self): """Setup skill settings.""" - LOG.debug(f"initializing skill settings for {self.skill_id}") + self.log.debug(f"initializing skill settings for {self.skill_id}") # NOTE: lock is disabled due to usage of deepcopy and to allow json serialization self._settings = JsonStorage(self._settings_path, disable_lock=True) - if self._initial_settings: - # TODO make a debug log in next version - LOG.warning("Copying default settings values defined in __init__ \n" - "Please move code from __init__() to initialize() " - "if you did not expect to see this message") + if self._initial_settings and not self._is_fully_initialized: + self.log.warning("Copying default settings values defined in __init__ \n" + f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") for k, v in self._initial_settings.items(): if k not in self._settings: self._settings[k] = v @@ -372,7 +377,6 @@ def _init_settings(self): # method not in mycroft-core def _init_skill_gui(self): try: - from mycroft.gui import SkillGUI self.gui = SkillGUI(self) self.gui.setup_default_handlers() except ImportError: @@ -428,9 +432,11 @@ def settings(self): if self._settings is not None: return self._settings else: - LOG.error('Skill not fully initialized. ' - 'Only default values can be set, no settings can be read or changed.' - 'Move code from __init__() to initialize() to correct this.') + self.log.warning('Skill not fully initialized. ' + 'Only default values can be set, no settings can be read or changed.' + f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + self.log.error(simple_trace(traceback.format_stack())) return self._initial_settings # not a property in mycroft-core @@ -455,9 +461,10 @@ def enclosure(self): if self._enclosure: return self._enclosure else: - LOG.error('Skill not fully initialized. Move code ' + - 'from __init__() to initialize() to correct this.') - LOG.error(simple_trace(traceback.format_stack())) + self.log.warning('Skill not fully initialized.' + f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + self.log.error(simple_trace(traceback.format_stack())) raise Exception('Accessed MycroftSkill.enclosure in __init__') # not a property in mycroft-core @@ -472,9 +479,10 @@ def file_system(self): if self._file_system: return self._file_system else: - LOG.error('Skill not fully initialized. Move code ' + - 'from __init__() to initialize() to correct this.') - LOG.error(simple_trace(traceback.format_stack())) + self.log.warning('Skill not fully initialized.' + f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + self.log.error(simple_trace(traceback.format_stack())) raise Exception('Accessed MycroftSkill.file_system in __init__') @file_system.setter @@ -488,9 +496,10 @@ def bus(self): if self._bus: return self._bus else: - LOG.error('Skill not fully initialized. Move code ' + - 'from __init__() to initialize() to correct this.') - LOG.error(simple_trace(traceback.format_stack())) + self.log.warning('Skill not fully initialized.' + f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + self.log.error(simple_trace(traceback.format_stack())) raise Exception('Accessed MycroftSkill.bus in __init__') @bus.setter @@ -669,7 +678,7 @@ def wrapper(message): for key in self.public_api: if ('type' in self.public_api[key] and 'func' in self.public_api[key]): - LOG.debug(f"Adding api method: {self.public_api[key]['type']}") + self.log.debug(f"Adding api method: {self.public_api[key]['type']}") # remove the function member since it shouldn't be # reused and can't be sent over the messagebus @@ -722,7 +731,7 @@ def handle_settings_change(self, message): """ remote_settings = message.data.get(self.skill_id) if remote_settings is not None: - LOG.info('Updating settings for skill ' + self.skill_id) + self.log.info('Updating settings for skill ' + self.skill_id) self.settings.update(**remote_settings) self.settings.store() if self.settings_change_callback is not None: @@ -1193,7 +1202,7 @@ def report_metric(self, name, data): if Configuration().get('opt_in', False): MetricsApi().report_metric(name, data) except Exception as e: - LOG.error(f'Metric couldn\'t be uploaded, due to a network error ({e})') + self.log.error(f'Metric couldn\'t be uploaded, due to a network error ({e})') def send_email(self, title, body): """Send an email to the registered user's email. @@ -1321,7 +1330,7 @@ def _on_event_error(self, error, message, handler_info, skill_data, speak_errors speech = get_dialog('skill.error', self.lang, msg_data) if speak_errors: self.speak(speech) - LOG.exception(error) + self.log.exception(error) # append exception information in message skill_data['exception'] = repr(error) if handler_info: @@ -1349,7 +1358,7 @@ def add_event(self, name, handler, handler_info=None, once=False, speak_errors=T def on_error(error, message): if isinstance(error, AbortEvent): - LOG.info("Skill execution aborted") + self.log.info("Skill execution aborted") self._on_event_end(message, handler_info, skill_data) return self._on_event_error(error, message, handler_info, skill_data, speak_errors) @@ -1507,7 +1516,7 @@ def disable_intent(self, intent_name): bool: True if disabled, False if it wasn't registered """ if intent_name in self.intent_service: - LOG.info('Disabling intent ' + intent_name) + self.log.info('Disabling intent ' + intent_name) name = f'{self.skill_id}:{intent_name}' self.intent_service.detach_intent(name) @@ -1517,7 +1526,7 @@ def disable_intent(self, intent_name): self.intent_service.detach_intent(lang_intent_name) return True else: - LOG.error(f'Could not disable {intent_name}, it hasn\'t been registered.') + self.log.error(f'Could not disable {intent_name}, it hasn\'t been registered.') return False def enable_intent(self, intent_name): @@ -1536,10 +1545,10 @@ def enable_intent(self, intent_name): else: intent.name = intent_name self.register_intent(intent, None) - LOG.debug(f'Enabling intent {intent_name}') + self.log.debug(f'Enabling intent {intent_name}') return True else: - LOG.error(f'Could not enable {intent_name}, it hasn\'t been registered.') + self.log.error(f'Could not enable {intent_name}, it hasn\'t been registered.') return False def set_context(self, context, word='', origin=''): @@ -1748,8 +1757,7 @@ def __handle_stop(self, message): {"by": "skill:" + self.skill_id}, {"skill_id": self.skill_id})) except Exception as e: - LOG.exception(e) - LOG.error(f'Failed to stop skill: {self.skill_id}') + self.log.exception(f'Failed to stop skill: {self.skill_id}') def stop(self): """Optional method implemented by subclass.""" @@ -1791,12 +1799,12 @@ def default_shutdown(self): try: self.stop() except Exception: - LOG.error(f'Failed to stop skill: {self.skill_id}', exc_info=True) + self.log.error(f'Failed to stop skill: {self.skill_id}', exc_info=True) try: self.shutdown() except Exception as e: - LOG.error(f'Skill specific shutdown function encountered an error: {e}') + self.log.error(f'Skill specific shutdown function encountered an error: {e}') self.bus.emit( Message('detach_skill', {'skill_id': str(self.skill_id) + ':'}, diff --git a/setup.py b/setup.py index d36b1f01..d5022914 100644 --- a/setup.py +++ b/setup.py @@ -65,5 +65,10 @@ def required(requirements_file): author='jarbasAi', author_email='jarbasai@mailfence.com', include_package_data=True, - description='frameworks, templates and patches for the mycroft universe' + description='frameworks, templates and patches for the OpenVoiceOS universe', + entry_points={ + 'console_scripts': [ + 'ovos-skill-launcher=ovos_workshop.skill_launcher:_launch_script' + ] + } ) From 25a2bbddcdac0c396f97f44998d76e64ade2ea7e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 20 Apr 2023 02:25:47 +0000 Subject: [PATCH 017/154] Increment Version to 0.0.12a7 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 550dae6c..f36ee506 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 6 +VERSION_ALPHA = 7 # END_VERSION_BLOCK From a95bd0353070fc3c76795efb48221fb12bde8007 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 20 Apr 2023 02:26:25 +0000 Subject: [PATCH 018/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b17d092e..ebf03541 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a6) (2023-04-20) +## [0.0.12a7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a7) (2023-04-20) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a5...0.0.12a6) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a6...0.0.12a7) + +**Implemented enhancements:** + +- feat/skill\_launcher [\#65](https://github.com/OpenVoiceOS/OVOS-workshop/pull/65) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a6) (2023-04-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a5...V0.0.12a6) **Merged pull requests:** From 103a064e44c8572b31d492f55a3f1aab98f5ce03 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Apr 2023 05:18:46 +0100 Subject: [PATCH 019/154] refactor/move_intent_decorators (#73) --- ovos_workshop/decorators/__init__.py | 88 ++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/ovos_workshop/decorators/__init__.py b/ovos_workshop/decorators/__init__.py index 4f382502..e135dd54 100644 --- a/ovos_workshop/decorators/__init__.py +++ b/ovos_workshop/decorators/__init__.py @@ -4,11 +4,88 @@ from ovos_workshop.decorators.converse import converse_handler from ovos_workshop.decorators.fallback_handler import fallback_handler from ovos_utils import classproperty +from functools import wraps try: from ovos_workshop.decorators.ocp import ocp_next, ocp_play, ocp_pause, ocp_resume, ocp_search, ocp_previous, ocp_featured_media except ImportError: pass # these imports are only available if extra requirements are installed +""" +Decorators for use with MycroftSkill methods + +Helper decorators for handling context from skills. +""" + + +def adds_context(context, words=''): + """Decorator adding context to the Adapt context manager. + + Args: + context (str): context Keyword to insert + words (str): optional string content of Keyword + """ + + def context_add_decorator(func): + @wraps(func) + def func_wrapper(*args, **kwargs): + ret = func(*args, **kwargs) + args[0].set_context(context, words) + return ret + + return func_wrapper + + return context_add_decorator + + +def removes_context(context): + """Decorator removing context from the Adapt context manager. + + Args: + context (str): Context keyword to remove + """ + + def context_removes_decorator(func): + @wraps(func) + def func_wrapper(*args, **kwargs): + ret = func(*args, **kwargs) + args[0].remove_context(context) + return ret + + return func_wrapper + + return context_removes_decorator + + +def intent_handler(intent_parser): + """Decorator for adding a method as an intent handler.""" + + def real_decorator(func): + # Store the intent_parser inside the function + # This will be used later to call register_intent + if not hasattr(func, 'intents'): + func.intents = [] + func.intents.append(intent_parser) + return func + + return real_decorator + + +def intent_file_handler(intent_file): + """Decorator for adding a method as an intent file handler. + + This decorator is deprecated, use intent_handler for the same effect. + """ + + def real_decorator(func): + # Store the intent_file inside the function + # This will be used later to call register_intent_file + if not hasattr(func, 'intent_files'): + func.intent_files = [] + func.intent_files.append(intent_file) + return func + + return real_decorator + def resting_screen_handler(name): """Decorator for adding a method as an resting screen handler. @@ -25,3 +102,14 @@ def real_decorator(func): return real_decorator + +def skill_api_method(func): + """Decorator for adding a method to the skill's public api. + + Methods with this decorator will be registered on the message bus + and an api object can be created for interaction with the skill. + """ + # tag the method by adding an api_method member to it + if not hasattr(func, 'api_method') and hasattr(func, '__name__'): + func.api_method = True + return func From 0f72e2af2cbc1fb359d506ebc3a1a62538b0eb33 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 04:19:01 +0000 Subject: [PATCH 020/154] Increment Version to 0.0.12a8 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index f36ee506..8dd6edf6 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 7 +VERSION_ALPHA = 8 # END_VERSION_BLOCK From 3a026ec1839c5442ef6e27e2e35499950dd7bb1d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 04:19:31 +0000 Subject: [PATCH 021/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf03541..e9c3513c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a7) (2023-04-20) +## [0.0.12a8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a8) (2023-04-21) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a6...0.0.12a7) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a7...0.0.12a8) + +**Merged pull requests:** + +- refactor/move\_intent\_decorators [\#73](https://github.com/OpenVoiceOS/OVOS-workshop/pull/73) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a7) (2023-04-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a6...V0.0.12a7) **Implemented enhancements:** From 011404b9ef1b2e97bc9b81c88a620debe4331b5d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:20:44 +0100 Subject: [PATCH 022/154] fix/skill_launcher_locale_init (#71) --- ovos_workshop/skill_launcher.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index a64e5da1..2954cdc9 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -11,6 +11,7 @@ from ovos_bus_client.message import Message from ovos_config.config import Configuration from ovos_config.locations import get_xdg_data_dirs, get_xdg_data_save_path +from ovos_config.locale import setup_locale from ovos_plugin_manager.skills import find_skill_plugins from ovos_utils import wait_for_exit_signal from ovos_utils.file_utils import FileWatcher @@ -473,10 +474,30 @@ def load(self, skill_class): return self.loaded -def launch_plugin_skill(skill_id): - """ run a plugin skill standalone """ +def _connect_to_core(): + setup_locale() # ensure any initializations and resource loading is handled bus = MessageBusClient() bus.run_in_thread() + bus.connected_event.wait() + connected = False + while not connected: + LOG.debug("checking skills service status") + response = bus.wait_for_response(Message(f'mycroft.skills.is_ready', + context={"source": "workshop", + "destination": "skills"})) + if response and response.data['status']: + connected = True + else: + LOG.warning("ovos-core does not seem to be running") + LOG.debug("connected to core") + return bus + + +def launch_plugin_skill(skill_id): + """ run a plugin skill standalone """ + + bus = _connect_to_core() + plugins = find_skill_plugins() if skill_id not in plugins: raise ValueError(f"unknown skill_id: {skill_id}") @@ -493,8 +514,9 @@ def launch_plugin_skill(skill_id): def launch_standalone_skill(skill_directory, skill_id): """ run a skill standalone from a directory """ - bus = MessageBusClient() - bus.run_in_thread() + + bus = _connect_to_core() + skill_loader = SkillLoader(bus, skill_directory, skill_id=skill_id) try: @@ -508,6 +530,7 @@ def launch_standalone_skill(skill_directory, skill_id): def _launch_script(): """USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]""" + if (args_count := len(sys.argv)) == 2: skill_id = sys.argv[1] From e0326840c6f51e2a5953ad89915b6962ef546661 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 17:21:02 +0000 Subject: [PATCH 023/154] Increment Version to 0.0.12a9 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 8dd6edf6..3758186c 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 8 +VERSION_ALPHA = 9 # END_VERSION_BLOCK From b00329195ebdf27a27c4cfb377187685e7fb694a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 17:21:31 +0000 Subject: [PATCH 024/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9c3513c..78608a76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a8) (2023-04-21) +## [0.0.12a9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a9) (2023-04-21) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a7...0.0.12a8) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a8...0.0.12a9) + +**Fixed bugs:** + +- fix/skill\_launcher\_locale\_init [\#71](https://github.com/OpenVoiceOS/OVOS-workshop/pull/71) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a8) (2023-04-21) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a7...V0.0.12a8) **Merged pull requests:** From ea6f45d3ffb3358de014add014b26502696870ea Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:11:57 +0100 Subject: [PATCH 025/154] fix/universal_skills_are_back (#72) --- ovos_workshop/skills/auto_translatable.py | 88 +++++++++++++---------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/ovos_workshop/skills/auto_translatable.py b/ovos_workshop/skills/auto_translatable.py index 33e0aa11..c2b9166b 100644 --- a/ovos_workshop/skills/auto_translatable.py +++ b/ovos_workshop/skills/auto_translatable.py @@ -1,58 +1,73 @@ +from ovos_config import Configuration +from ovos_plugin_manager.language import OVOSLangDetectionFactory, OVOSLangTranslationFactory from ovos_utils import get_handler_name from ovos_utils.log import LOG +from ovos_workshop.resource_files import SkillResources from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill -try: - # TODO: Below methods are not defined in ovos_utils - from ovos_utils.lang.translate import detect_lang, translate_text -except ImportError as e: - detect_lang = None - translate_text = None - LOG.exception(e) - class UniversalSkill(OVOSSkill): ''' Skill that auto translates input/output from any language ''' def __init__(self, *args, **kwargs): - if not detect_lang and translate_text: - raise NotImplementedError("Translate methods not yet implemented") super().__init__(*args, **kwargs) - self.input_lang = self.lang - self.translate_keys = [] - self.translate_tags = True + self.lang_detector = OVOSLangDetectionFactory.create() + self.translator = OVOSLangTranslationFactory.create() + + self.internal_language = None # the skill internally only works in this language + self.translate_tags = True # __tags__ private value will be translated (adapt entities) + self.translate_keys = [] # any keys added here will have values translated in message.data + if self.internal_language is None: + lang = Configuration().get("lang", "en-us") + LOG.warning(f"UniversalSkill are expected to specify their internal_language, casting to {lang}") + self.internal_language = lang + + def _load_lang(self, root_directory=None, lang=None): + """unlike base skill class all resources are in self.internal_language by default + instead of self.lang (which comes from message) + this ensures things like self.dialog_render reflect self.internal_lang + """ + lang = lang or self.internal_language # self.lang in base class + root_directory = root_directory or self.res_dir + if lang not in self._lang_resources: + self._lang_resources[lang] = SkillResources(root_directory, lang, skill_id=self.skill_id) + return self._lang_resources[lang] def detect_language(self, utterance): try: - return detect_lang(utterance) + return self.lang_detector.detect(utterance) except: + # self.lang to account for lang defined in message return self.lang.split("-")[0] - def translate(self, text, lang=None): - lang = lang or self.lang - translated = translate_text(text, lang) - LOG.info("translated " + text + " to " + translated) - return translated - - def _translate_utterance(self, utterance="", lang=None): - lang = lang or self.input_lang - if utterance and lang is not None: - ut_lang = self.detect_language(utterance) - if lang.split("-")[0] != ut_lang: - utterance = self.translate(utterance, lang) - return utterance + def translate_utterance(self, text, target_lang, sauce_lang=None): + sauce_lang = sauce_lang or self.detect_language(text) + if sauce_lang.split("-")[0] != target_lang: + translated = self.translator.translate(text, source=sauce_lang, target=target_lang) + LOG.info("translated " + text + " to " + translated) + return translated + return text def _translate_message(self, message): + # translate speech from input lang to internal lang + sauce_lang = self.lang # from message or config + out_lang = self.internal_language # skill wants input is in this language, + ut = message.data.get("utterance") if ut: - message.data["utterance"] = self._translate_utterance(ut) + message.data["utterance"] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) + if "utterances" in message.data: + message.data["utterances"] = [self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) + for ut in message.data["utterances"]] for key in self.translate_keys: if key in message.data: ut = message.data[key] - message.data[key] = self._translate_utterance(ut) + message.data[key] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) if self.translate_tags: for idx, token in enumerate(message.data["__tags__"]): - message.data["__tags__"][idx] = self._translate_utterance(token.get("key", "")) + message.data["__tags__"][idx] = self.translate_utterance(token.get("key", ""), + target_lang=out_lang, + sauce_lang=sauce_lang) return message def create_universal_handler(self, handler): @@ -72,16 +87,18 @@ def register_intent_file(self, intent_file, handler): handler = self.create_universal_handler(handler) super().register_intent_file(intent_file, handler) - def speak(self, utterance, expect_response=False, wait=False): - utterance = self._translate_utterance(utterance) - super().speak(utterance, expect_response, wait) + def speak(self, utterance, *args, **kwargs): + # translate speech from input lang to output lang + out_lang = self.lang # from message or config + sauce_lang = self.internal_language # skill output is in this language + utterance = self.translate_utterance(utterance, sauce_lang, out_lang) + super().speak(utterance, *args, **kwargs) class UniversalFallback(UniversalSkill, OVOSFallbackSkill): ''' Fallback Skill that auto translates input/output from any language ''' def create_universal_fallback_handler(self, handler): - def universal_fallback_handler(message): # auto_Translate input message = self._translate_message(message) @@ -95,5 +112,4 @@ def universal_fallback_handler(message): def register_fallback(self, handler, priority): handler = self.create_universal_fallback_handler(handler) - self.instance_fallback_handlers.append(handler) - self._register_fallback(handler, priority) + super().register_fallback(handler, priority) From 0070fd182cb943f7865a75ad5be58b04711b49c8 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 22:12:19 +0000 Subject: [PATCH 026/154] Increment Version to 0.0.12a10 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 3758186c..95f3407e 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 9 +VERSION_ALPHA = 10 # END_VERSION_BLOCK From 282d13d0fe02b83326d3ea2e9d54a45a2739a488 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 21 Apr 2023 22:12:46 +0000 Subject: [PATCH 027/154] Update Changelog --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78608a76..aeccf128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # Changelog -## [0.0.12a9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a9) (2023-04-21) +## [0.0.12a10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a10) (2023-04-21) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a8...0.0.12a9) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a9...0.0.12a10) + +**Fixed bugs:** + +- ERROR - No module named 'ovos\_utils.lang.translate' [\#67](https://github.com/OpenVoiceOS/OVOS-workshop/issues/67) +- fix/universal\_skills\_are\_back [\#72](https://github.com/OpenVoiceOS/OVOS-workshop/pull/72) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a9) (2023-04-21) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a8...V0.0.12a9) **Fixed bugs:** From 1149a97be47ab20701c502d18d24784e8d86ce90 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 22 Apr 2023 01:17:27 +0100 Subject: [PATCH 028/154] feat/auto_tx_skills_continued (#74) --- ovos_workshop/skills/auto_translatable.py | 134 +++++++++++++++++++-- ovos_workshop/skills/base.py | 10 ++ ovos_workshop/skills/common_query_skill.py | 43 ++++--- 3 files changed, 160 insertions(+), 27 deletions(-) diff --git a/ovos_workshop/skills/auto_translatable.py b/ovos_workshop/skills/auto_translatable.py index c2b9166b..0e8b0c05 100644 --- a/ovos_workshop/skills/auto_translatable.py +++ b/ovos_workshop/skills/auto_translatable.py @@ -2,12 +2,23 @@ from ovos_plugin_manager.language import OVOSLangDetectionFactory, OVOSLangTranslationFactory from ovos_utils import get_handler_name from ovos_utils.log import LOG + from ovos_workshop.resource_files import SkillResources +from ovos_workshop.skills.common_query_skill import CommonQuerySkill from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill class UniversalSkill(OVOSSkill): - ''' Skill that auto translates input/output from any language ''' + ''' Skill that auto translates input/output from any language + + intent handlers are ensured to receive utterances in self.internal_language + intent handlers are expected to produce utterances in self.internal_language + + self.speak will always translate utterances from self.internal_lang to self.lang + + NOTE: self.lang reflects the original query language + but received utterances are always in self.internal_language + ''' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -16,7 +27,11 @@ def __init__(self, *args, **kwargs): self.internal_language = None # the skill internally only works in this language self.translate_tags = True # __tags__ private value will be translated (adapt entities) - self.translate_keys = [] # any keys added here will have values translated in message.data + self.translate_keys = ["utterance", "utterances"] # keys added here will have values translated in message.data + + # autodetect will detect the lang of the utterance regardless of what has been reported + # to test just type in the cli in another language and watch answers still coming + self.autodetect = False # TODO from mycroft.conf if self.internal_language is None: lang = Configuration().get("lang", "en-us") LOG.warning(f"UniversalSkill are expected to specify their internal_language, casting to {lang}") @@ -41,7 +56,10 @@ def detect_language(self, utterance): return self.lang.split("-")[0] def translate_utterance(self, text, target_lang, sauce_lang=None): - sauce_lang = sauce_lang or self.detect_language(text) + if self.autodetect: + sauce_lang = self.detect_language(text) + else: + sauce_lang = sauce_lang or self.detect_language(text) if sauce_lang.split("-")[0] != target_lang: translated = self.translator.translate(text, source=sauce_lang, target=target_lang) LOG.info("translated " + text + " to " + translated) @@ -53,21 +71,37 @@ def _translate_message(self, message): sauce_lang = self.lang # from message or config out_lang = self.internal_language # skill wants input is in this language, - ut = message.data.get("utterance") - if ut: - message.data["utterance"] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) - if "utterances" in message.data: - message.data["utterances"] = [self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) - for ut in message.data["utterances"]] + if sauce_lang == out_lang and not self.autodetect: + # do nothing + return message + + translation_data = {"original": {}, "translated": {}, + "source_lang": sauce_lang, "internal_lang": self.internal_language} + + def _do_tx(thing): + if isinstance(thing, str): + thing = self.translate_utterance(thing, target_lang=out_lang, sauce_lang=sauce_lang) + elif isinstance(thing, list): + thing = [_do_tx(t) for t in thing] + elif isinstance(thing, dict): + thing = {k: _do_tx(v) for k, v in thing.items()} + return thing + for key in self.translate_keys: if key in message.data: - ut = message.data[key] - message.data[key] = self.translate_utterance(ut, target_lang=out_lang, sauce_lang=sauce_lang) + translation_data["original"][key] = message.data[key] + translation_data["translated"][key] = message.data[key] = _do_tx(message.data[key]) + + # special case if self.translate_tags: + translation_data["original"]["__tags__"] = message.data["__tags__"] for idx, token in enumerate(message.data["__tags__"]): message.data["__tags__"][idx] = self.translate_utterance(token.get("key", ""), target_lang=out_lang, sauce_lang=sauce_lang) + translation_data["translated"]["__tags__"] = message.data["__tags__"] + + message.context["translation_data"] = translation_data return message def create_universal_handler(self, handler): @@ -91,12 +125,31 @@ def speak(self, utterance, *args, **kwargs): # translate speech from input lang to output lang out_lang = self.lang # from message or config sauce_lang = self.internal_language # skill output is in this language - utterance = self.translate_utterance(utterance, sauce_lang, out_lang) + if out_lang != sauce_lang or self.autodetect: + meta = kwargs.get("meta") or {} + meta["translation_data"] = { + "original": utterance, + "internal_lang": self.internal_language, + "target_lang": out_lang + } + utterance = self.translate_utterance(utterance, sauce_lang, out_lang) + meta["translation_data"]["translated"] = utterance + kwargs["meta"] = meta super().speak(utterance, *args, **kwargs) class UniversalFallback(UniversalSkill, OVOSFallbackSkill): - ''' Fallback Skill that auto translates input/output from any language ''' + ''' Fallback Skill that auto translates input/output from any language + + fallback handlers are ensured to receive utterances in self.internal_language + fallback handlers are expected to produce utterances in self.internal_language + + self.speak will always translate utterances from self.internal_lang to self.lang + + NOTE: self.lang reflects the original query language + but received utterances are always in self.internal_language + + ''' def create_universal_fallback_handler(self, handler): def universal_fallback_handler(message): @@ -113,3 +166,58 @@ def universal_fallback_handler(message): def register_fallback(self, handler, priority): handler = self.create_universal_fallback_handler(handler) super().register_fallback(handler, priority) + + +class UniversalCommonQuerySkill(UniversalSkill, CommonQuerySkill): + ''' CommonQuerySkill that auto translates input/output from any language + + CQS_match_query_phrase and CQS_action are ensured to received phrase in self.internal_language + + CQS_match_query_phrase is assumed to return a response in self.internal_lang + it will be translated back before speaking + + self.speak will always translate utterances from self.internal_lang to self.lang + + NOTE: self.lang reflects the original query language + but received utterances are always in self.internal_language + ''' + + def __handle_query_action(self, message): + """Message handler for question:action. + + Extracts phrase and data from message forward this to the skills + CQS_action method. + """ + if message.data["skill_id"] != self.skill_id: + # Not for this skill! + return + if self.lang != self.internal_language or self.autodetect: + message.data["phrase"] = self.translate_utterance(message.data["phrase"], + sauce_lang=self.lang, + target_lang=self.internal_language) + + super().__handle_query_action(message) + + def __get_cq(self, search_phrase): + if self.lang == self.internal_language and not self.autodetect: + return super().__get_cq(search_phrase) + + # convert input into internal lang + search_phrase = self.translate_utterance(search_phrase, self.internal_language, self.lang) + result = super().__get_cq(search_phrase) + if not result: + return None + answer = result[2] + # convert response back into source lang + answer = self.translate_utterance(answer, self.lang, self.internal_language) + if len(result) > 3: + # optional callback_data + result = (result[0], result[1], answer, result[3]) + else: + result = (result[0], result[1], answer) + return result + + def remove_noise(self, phrase, lang=None): + """remove noise to produce essence of question""" + return super().remove_noise(phrase, self.internal_language) + diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 64b8326e..4c64486a 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -41,6 +41,7 @@ from ovos_utils.intents import ConverseTracker from ovos_utils.intents import Intent, IntentBuilder from ovos_utils.intents.intent_service_interface import munge_regex, munge_intent_parser, IntentServiceInterface +from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang from ovos_utils.parse import match_one @@ -1652,10 +1653,19 @@ def speak(self, utterance, expect_response=False, wait=False, meta=None): 'expect_response': expect_response, 'meta': meta, 'lang': self.lang} + + # grab message that triggered speech so we can keep context message = dig_for_message() m = message.forward("speak", data) if message \ else Message("speak", data) m.context["skill_id"] = self.skill_id + + # update any auto-translation metadata in message.context + if "translation_data" in meta: + tx_data = merge_dict(m.context.get("translation_data", {}), + meta["translation_data"]) + m.context["translation_data"] = tx_data + self.bus.emit(m) if wait: diff --git a/ovos_workshop/skills/common_query_skill.py b/ovos_workshop/skills/common_query_skill.py index d6860bb8..affbaab3 100644 --- a/ovos_workshop/skills/common_query_skill.py +++ b/ovos_workshop/skills/common_query_skill.py @@ -62,11 +62,12 @@ def __init__(self, name=None, bus=None): default_res = f"{dirname(dirname(__file__))}/res/text/{self.lang}/noise_words.list" noise_words_filename = resolve_resource_file(noise_words_filepath) or \ resolve_resource_file(default_res) - self.translated_noise_words = [] + + self._translated_noise_words = {} if noise_words_filename: with open(noise_words_filename) as f: - self.translated_noise_words = f.read().strip() - self.translated_noise_words = self.translated_noise_words.split() + translated_noise_words = f.read().strip() + self._translated_noise_words[self.lang] = translated_noise_words.split() # these should probably be configurable self.level_confidence = { @@ -75,6 +76,16 @@ def __init__(self, name=None, bus=None): CQSMatchLevel.GENERAL: 0.5 } + @property + def translated_noise_words(self): + LOG.warning("self.translated_noise_words will become a private variable in next release") + return self._translated_noise_words.get(self.lang, []) + + @translated_noise_words.setter + def translated_noise_words(self, val): + LOG.warning("self.translated_noise_words will become a private variable in next release") + self._translated_noise_words[self.lang] = val + def bind(self, bus): """Overrides the default bind method of MycroftSkill. @@ -95,20 +106,14 @@ def __handle_question_query(self, message): "skill_id": self.skill_id, "searching": True})) - # Now invoke the CQS handler to let the skill perform its search - try: - result = self.CQS_match_query_phrase(search_phrase) - except: - LOG.exception(f"error matching {search_phrase} with {self.skill_id}") - result = None + result = self.__get_cq(search_phrase) if result: match = result[0] level = result[1] answer = result[2] callback = result[3] if len(result) > 3 else None - confidence = self.__calc_confidence( - match, search_phrase, level, answer) + confidence = self.__calc_confidence(match, search_phrase, level, answer) self.bus.emit(message.response({"phrase": search_phrase, "skill_id": self.skill_id, "answer": answer, @@ -120,10 +125,20 @@ def __handle_question_query(self, message): "skill_id": self.skill_id, "searching": False})) - def remove_noise(self, phrase): + def __get_cq(self, search_phrase): + # Now invoke the CQS handler to let the skill perform its search + try: + result = self.CQS_match_query_phrase(search_phrase) + except: + LOG.exception(f"error matching {search_phrase} with {self.skill_id}") + result = None + return result + + def remove_noise(self, phrase, lang=None): """remove noise to produce essence of question""" + lang = lang or self.lang phrase = ' ' + phrase + ' ' - for word in self.translated_noise_words: + for word in self._translated_noise_words.get(lang, []): mtch = ' ' + word + ' ' if phrase.find(mtch) > -1: phrase = phrase.replace(mtch, " ") @@ -183,7 +198,7 @@ def __handle_query_action(self, message): @abstractmethod def CQS_match_query_phrase(self, phrase): - """Analyze phrase to see if it is a play-able phrase with this skill. + """Analyze phrase to see if it is a answer-able phrase with this skill. Needs to be implemented by the skill. From c8a0868f71b3999230eeef1be62710f176c15054 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 00:17:49 +0000 Subject: [PATCH 029/154] Increment Version to 0.0.12a11 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 95f3407e..1f824dee 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 10 +VERSION_ALPHA = 11 # END_VERSION_BLOCK From 1351c39acc32bfad913d09245a89aa45eba7bbb7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 00:18:18 +0000 Subject: [PATCH 030/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeccf128..c02e018d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a10) (2023-04-21) +## [0.0.12a11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a11) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a9...0.0.12a10) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a10...0.0.12a11) + +**Implemented enhancements:** + +- feat/auto\_tx\_skills\_continued [\#74](https://github.com/OpenVoiceOS/OVOS-workshop/pull/74) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a10) (2023-04-21) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a9...V0.0.12a10) **Fixed bugs:** From 8bad3912ac3aae0c9448a6cdc196cd2166f92c60 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 22 Apr 2023 02:04:24 +0100 Subject: [PATCH 031/154] fix/classic core checks (#75) --- ovos_workshop/skills/base.py | 18 ++++++++++++++---- ovos_workshop/skills/mycroft_skill.py | 11 +---------- ovos_workshop/skills/ovos.py | 27 +++++++++++++-------------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 4c64486a..53518cd2 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -66,6 +66,19 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) +def is_classic_core(): + """ Check if the current core is the classic mycroft-core """ + try: + from mycroft.version import OVOS_VERSION_STR + return False # ovos-core + except ImportError: + try: + from mycroft.version import CORE_VERSION_STR + return True # mycroft-core + except ImportError: + return False # standalone + + class SkillGUI(GUIInterface): """SkillGUI - Interface to the Graphical User Interface @@ -632,11 +645,8 @@ def bind(self, bus): self._register_system_event_handlers() self._register_public_api() - try: - from mycroft.version import OVOS_VERSION_STR - except ImportError: + if is_classic_core(): # inject ovos exclusive features in vanila mycroft-core if possible - ## limited support for missing skill deactivated event # TODO - update ConverseTracker ConverseTracker.connect_bus(self.bus) # pull/1468 diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py index 8b60d1cb..314b0cd5 100644 --- a/ovos_workshop/skills/mycroft_skill.py +++ b/ovos_workshop/skills/mycroft_skill.py @@ -21,16 +21,7 @@ from ovos_config.locations import get_xdg_config_save_path from ovos_utils.log import LOG -from ovos_workshop.skills.base import BaseSkill - - -def is_classic_core(): - """ Check if the current core is the classic mycroft-core """ - try: - from mycroft.version import OVOS_VERSION_STR - return False - except ImportError: - return True +from ovos_workshop.skills.base import BaseSkill, is_classic_core class _SkillMetaclass(ABCMeta): diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 68e3dcbf..80817457 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -13,7 +13,7 @@ from ovos_workshop.decorators.killable import killable_event, \ AbortQuestion from ovos_workshop.skills.layers import IntentLayers -from ovos_workshop.skills.mycroft_skill import MycroftSkill +from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core class OVOSSkill(MycroftSkill): @@ -70,16 +70,17 @@ def deactivate(self): self._deactivate() def play_audio(self, filename): - try: - from mycroft.version import OVOS_VERSION_BUILD, OVOS_VERSION_MINOR, OVOS_VERSION_MAJOR - if OVOS_VERSION_MAJOR >= 1 or \ - OVOS_VERSION_MINOR > 0 or \ - OVOS_VERSION_BUILD >= 4: - self.bus.emit(Message("mycroft.audio.queue", - {"filename": filename})) - return - except: - pass + if not is_classic_core(): + try: + from mycroft.version import OVOS_VERSION_BUILD, OVOS_VERSION_MINOR, OVOS_VERSION_MAJOR + if OVOS_VERSION_MAJOR >= 1 or \ + OVOS_VERSION_MINOR > 0 or \ + OVOS_VERSION_BUILD >= 4: + self.bus.emit(Message("mycroft.audio.queue", + {"filename": filename})) + return + except: + pass LOG.warning("self.play_audio requires ovos-core >= 0.0.4a45, falling back to local skill playback") play_audio(filename).wait() @@ -202,9 +203,7 @@ def send_stop_signal(self, stop_event=None): self.bus.emit(msg.forward('recognizer_loop:record_stop')) # special non-ovos handling - try: - from mycroft.version import OVOS_VERSION_STR - except ImportError: + if is_classic_core(): # NOTE: mycroft does not have an event to stop recording # this attempts to force a stop by sending silence to end STT step self.bus.emit(Message('mycroft.mic.mute')) From 205adb2405f525980a609d18dd590e197dcfda22 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 01:04:38 +0000 Subject: [PATCH 032/154] Increment Version to 0.0.12a12 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 1f824dee..f93e52c3 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 11 +VERSION_ALPHA = 12 # END_VERSION_BLOCK From c69f6ae8f2a23a0af027772668844f77322deb7f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 01:05:12 +0000 Subject: [PATCH 033/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c02e018d..9bbb4859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a11) (2023-04-22) +## [0.0.12a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a12) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a10...0.0.12a11) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a11...0.0.12a12) + +**Fixed bugs:** + +- fix/classic core checks [\#75](https://github.com/OpenVoiceOS/OVOS-workshop/pull/75) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a11) (2023-04-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a10...V0.0.12a11) **Implemented enhancements:** From 4b586d2e1c3145998997e100b65d2127273839b0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 22 Apr 2023 04:24:08 +0100 Subject: [PATCH 034/154] fix/is_classic_core2 (#76) --- .github/workflows/unit_tests.yml | 2 +- ovos_workshop/decorators/ocp.py | 133 +++++++++++++++- ovos_workshop/resource_files.py | 12 +- ovos_workshop/settings.py | 159 +++++++++++++++++++ ovos_workshop/skills/base.py | 7 +- ovos_workshop/skills/common_play.py | 7 +- ovos_workshop/skills/mycroft_skill.py | 53 +++---- ovos_workshop/skills/ovos.py | 3 +- test/unittests/ovos_tskill_abort/__init__.py | 2 +- test/unittests/skills/mocks.py | 2 +- test/unittests/skills/test_skill/__init__.py | 2 +- test/unittests/test_abort.py | 2 +- test/unittests/test_skill.py | 2 +- test/unittests/test_skill_classes.py | 4 +- 14 files changed, 340 insertions(+), 50 deletions(-) create mode 100644 ovos_workshop/settings.py diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 0cc68ec1..eccc5c24 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -35,7 +35,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.8, 3.9, "3.10", "3.11" ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/ovos_workshop/decorators/ocp.py b/ovos_workshop/decorators/ocp.py index 74350e7f..532fad71 100644 --- a/ovos_workshop/decorators/ocp.py +++ b/ovos_workshop/decorators/ocp.py @@ -1,7 +1,3 @@ -from functools import wraps -from ovos_workshop.decorators.layers import IntentLayers -from ovos_plugin_common_play.ocp import * -from ovos_plugin_common_play.ocp.status import * def ocp_search(): @@ -115,3 +111,132 @@ def real_decorator(func): return func return real_decorator + + +try: + from ovos_plugin_common_play.ocp.status import MediaType, PlayerState, MediaState, MatchConfidence, \ + PlaybackType, PlaybackMode, LoopState, TrackState +except ImportError: + + # TODO - manually keep these in sync as needed + # apps interfacing with OCP need the enums, + # but they are native to OCP does not make sense for OCP to import them from here, + # therefore we duplicate them when needed + from enum import IntEnum + + + class MatchConfidence(IntEnum): + EXACT = 95 + VERY_HIGH = 90 + HIGH = 80 + AVERAGE_HIGH = 70 + AVERAGE = 50 + AVERAGE_LOW = 30 + LOW = 15 + VERY_LOW = 1 + + + class TrackState(IntEnum): + DISAMBIGUATION = 1 # media result, not queued for playback + + PLAYING_SKILL = 20 # Skill is handling playback internally + PLAYING_AUDIOSERVICE = 21 # Skill forwarded playback to audio service + PLAYING_VIDEO = 22 # Skill forwarded playback to gui player + PLAYING_AUDIO = 23 # Skill forwarded audio playback to gui player + PLAYING_MPRIS = 24 # External media player is handling playback + PLAYING_WEBVIEW = 25 # Media playback handled in browser (eg. javascript) + + QUEUED_SKILL = 30 # Waiting playback to be handled inside skill + QUEUED_AUDIOSERVICE = 31 # Waiting playback in audio service + QUEUED_VIDEO = 32 # Waiting playback in gui + QUEUED_AUDIO = 33 # Waiting playback in gui + QUEUED_WEBVIEW = 34 # Waiting playback in gui + + + class MediaState(IntEnum): + # https://doc.qt.io/qt-5/qmediaplayer.html#MediaStatus-enum + # The status of the media cannot be determined. + UNKNOWN = 0 + # There is no current media. PlayerState == STOPPED + NO_MEDIA = 1 + # The current media is being loaded. The player may be in any state. + LOADING_MEDIA = 2 + # The current media has been loaded. PlayerState== STOPPED + LOADED_MEDIA = 3 + # Playback of the current media has stalled due to + # insufficient buffering or some other temporary interruption. + # PlayerState != STOPPED + STALLED_MEDIA = 4 + # The player is buffering data but has enough data buffered + # for playback to continue for the immediate future. + # PlayerState != STOPPED + BUFFERING_MEDIA = 5 + # The player has fully buffered the current media. PlayerState != STOPPED + BUFFERED_MEDIA = 6 + # Playback has reached the end of the current media. PlayerState == STOPPED + END_OF_MEDIA = 7 + # The current media cannot be played. PlayerState == STOPPED + INVALID_MEDIA = 8 + + + class PlayerState(IntEnum): + # https://doc.qt.io/qt-5/qmediaplayer.html#State-enum + STOPPED = 0 + PLAYING = 1 + PAUSED = 2 + + + class LoopState(IntEnum): + NONE = 0 + REPEAT = 1 + REPEAT_TRACK = 2 + + + class PlaybackType(IntEnum): + SKILL = 0 # skills handle playback whatever way they see fit, + # eg spotify / mycroft common play + VIDEO = 1 # Video results + AUDIO = 2 # Results should be played audio only + AUDIO_SERVICE = 3 # Results should be played without using the GUI + MPRIS = 4 # External MPRIS compliant player + WEBVIEW = 5 # GUI webview, render a url instead of media player + UNDEFINED = 100 # data not available, hopefully status will be updated soon.. + + + class PlaybackMode(IntEnum): + AUTO = 0 # play each entry as considered appropriate, + # ie, make it happen the best way possible + AUDIO_ONLY = 10 # only consider audio entries + VIDEO_ONLY = 20 # only consider video entries + FORCE_AUDIO = 30 # cast video to audio unconditionally + # (audio can still play in mycroft-gui) + FORCE_AUDIOSERVICE = 40 # cast everything to audio service backend, + # mycroft-gui will not be used + EVENTS_ONLY = 50 # only emit ocp events, do not display or play anything. + # allows integration with external interfaces + + + class MediaType(IntEnum): + GENERIC = 0 + AUDIO = 1 + MUSIC = 2 + VIDEO = 3 + AUDIOBOOK = 4 + GAME = 5 + PODCAST = 6 + RADIO = 7 + NEWS = 8 + TV = 9 + MOVIE = 10 + TRAILER = 11 + VISUAL_STORY = 13 + BEHIND_THE_SCENES = 14 + DOCUMENTARY = 15 + RADIO_THEATRE = 16 + SHORT_FILM = 17 + SILENT_MOVIE = 18 + BLACK_WHITE_MOVIE = 20 + CARTOON = 21 + + ADULT = 69 + HENTAI = 70 diff --git a/ovos_workshop/resource_files.py b/ovos_workshop/resource_files.py index d576dc05..39700a80 100644 --- a/ovos_workshop/resource_files.py +++ b/ovos_workshop/resource_files.py @@ -17,6 +17,7 @@ import re from collections import namedtuple from os import walk +from os.path import dirname from pathlib import Path from typing import List, Optional, Tuple @@ -678,8 +679,11 @@ def _make_unique_regex_group( class CoreResources(SkillResources): def __init__(self, language): - from mycroft import MYCROFT_ROOT_PATH - directory = f"{MYCROFT_ROOT_PATH}/mycroft/res" + try: + from mycroft import MYCROFT_ROOT_PATH + directory = f"{MYCROFT_ROOT_PATH}/mycroft/res" + except ImportError: + directory = f"{dirname(__file__)}/res" super().__init__(directory, language) @@ -818,6 +822,10 @@ def resolve_resource_file(res_name): if os.path.isfile(filename): return filename + filename = f"{dirname(__file__)}/res" + if os.path.isfile(filename): + return filename + # Finally look for it in the ovos-core package try: from mycroft import MYCROFT_ROOT_PATH diff --git a/ovos_workshop/settings.py b/ovos_workshop/settings.py new file mode 100644 index 00000000..4bb9617f --- /dev/null +++ b/ovos_workshop/settings.py @@ -0,0 +1,159 @@ +import json +from os.path import isfile +from threading import Timer + +import yaml +from ovos_backend_client.api import DeviceApi +from ovos_backend_client.pairing import is_paired, requires_backend +from ovos_backend_client.settings import RemoteSkillSettings, get_display_name +from ovos_bus_client.message import Message, dig_for_message +from ovos_utils.log import LOG + + +class SkillSettingsManager: + def __init__(self, skill): + self.download_timer = None + self.skill = skill + self.api = DeviceApi() + self.remote_settings = RemoteSkillSettings(self.skill_id, + settings=dict(self.skill.settings), + meta=self.load_meta(), + remote_id=self.skill_gid) + self.register_bus_handlers() + + def start(self): + self._download() + + def _download(self): + # If this method is called outside of the timer loop, ensure the + # existing timer is canceled before starting a new one. + if self.download_timer: + self.download_timer.cancel() + + self.download() + + # prepare to download again in 60 seconds + self.download_timer = Timer(60, self._download) + self.download_timer.daemon = True + self.download_timer.start() + + def stop(self): + # If this method is called outside of the timer loop, ensure the + # existing timer is canceled + if self.download_timer: + self.download_timer.cancel() + + @property + def bus(self): + return self.skill.bus + + @property + def skill_id(self): + return self.skill.skill_id + + @property + def display_name(self): + return get_display_name(self.skill_id) + + @property + def skill_gid(self): + return f"@{self.api.uuid}|{self.skill_id}" + + @property + def skill_meta(self): + return self.remote_settings.meta + + def register_bus_handlers(self): + self.skill.add_event('mycroft.skills.settings.update', + self.handle_download_remote) # backwards compat + self.skill.add_event('mycroft.skills.settings.download', + self.handle_download_remote) + self.skill.add_event('mycroft.skills.settings.upload', + self.handle_upload_local) + self.skill.add_event('mycroft.skills.settings.upload.meta', + self.handle_upload_meta) + self.skill.add_event('mycroft.paired', + self.handle_upload_local) + + def load_meta(self): + json_path = f"{self.skill.root_dir}/settingsmeta.json" + yaml_path = f"{self.skill.root_dir}/settingsmeta.yaml" + if isfile(yaml_path): + with open(yaml_path) as meta_file: + return yaml.safe_load(meta_file) + elif isfile(json_path): + with open(json_path) as meta_file: + return json.load(meta_file) + return {} + + def save_meta(self, generate=False): + # unset reload flag to avoid a reload on settingmeta change + # TODO - support for settingsmeta XDG paths + reload = self.skill.reload_skill + self.skill.reload_skill = False + + # generate meta for missing fields + if generate: + self.remote_settings.generate_meta() + + # write to disk + json_path = f"{self.skill.root_dir}/settingsmeta.json" + yaml_path = f"{self.skill.root_dir}/settingsmeta.yaml" + if isfile(yaml_path): + with open(yaml_path) as meta_file: + yaml.dump(self.remote_settings.meta, meta_file) + else: + with open(json_path, "w") as meta_file: + json.dump(self.remote_settings.meta, meta_file) + + # reset reloading flag + self.skill.reload_skill = reload + + @requires_backend + def upload(self, generate=False): + if not is_paired(): + LOG.error("Device needs to be paired to upload settings") + return + self.remote_settings.settings = dict(self.skill.settings) + if generate: + self.remote_settings.generate_meta() + self.remote_settings.upload() + + @requires_backend + def upload_meta(self, generate=False): + if not is_paired(): + LOG.error("Device needs to be paired to upload settingsmeta") + return + if generate: + self.remote_settings.settings = dict(self.skill.settings) + self.remote_settings.generate_meta() + self.remote_settings.upload_meta() + + @requires_backend + def download(self): + if not is_paired(): + LOG.error("Device needs to be paired to download remote settings") + return + self.remote_settings.download() + # we do not update skill object settings directly + # skill will handle the event and trigger a callback + if self.skill.settings != self.remote_settings.settings: + # dig old message to keep context + msg = dig_for_message() or Message("") + msg = msg.forward('mycroft.skills.settings.changed') + + msg.data[self.skill_id] = self.remote_settings.settings + self.bus.emit(msg) + + def handle_upload_meta(self, message): + skill_id = message.data.get("skill_id") + if skill_id == self.skill_id: + self.upload_meta() + + def handle_upload_local(self, message): + skill_id = message.data.get("skill_id") + if skill_id == self.skill_id: + self.upload() + + def handle_download_remote(self, message): + self.download() diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 53518cd2..c942acad 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -56,6 +56,7 @@ from ovos_workshop.filesystem import FileSystemAccess from ovos_workshop.resource_files import ResourceFile, \ CoreResources, SkillResources, find_resource +from ovos_workshop.settings import SkillSettingsManager # backwards compat alias @@ -400,11 +401,7 @@ def _init_skill_gui(self): # method not in mycroft-core def _init_settings_manager(self): - try: - from mycroft.skills.settings import SkillSettingsManager - self.settings_manager = SkillSettingsManager(self) - except ImportError: - pass + self.settings_manager = SkillSettingsManager(self) # method not present in mycroft-core def _start_filewatcher(self): diff --git a/ovos_workshop/skills/common_play.py b/ovos_workshop/skills/common_play.py index fc5b4607..50a4a413 100644 --- a/ovos_workshop/skills/common_play.py +++ b/ovos_workshop/skills/common_play.py @@ -1,11 +1,16 @@ from inspect import signature from threading import Event -from ovos_workshop.decorators.ocp import * from ovos_workshop.skills.ovos import OVOSSkill, MycroftSkill from ovos_bus_client import Message from ovos_utils.log import LOG +# backwards compat imports, do not delete, skills import from here +from ovos_workshop.decorators.ocp import ocp_play, ocp_next, ocp_pause, ocp_resume, ocp_search, \ + ocp_previous, ocp_featured_media, MediaType, MediaState, MatchConfidence, \ + PlaybackType, PlaybackMode, PlayerState, LoopState, TrackState + + def get_non_properties(obj): """Get attibutes that are not properties from object. diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py index 314b0cd5..b86a8c83 100644 --- a/ovos_workshop/skills/mycroft_skill.py +++ b/ovos_workshop/skills/mycroft_skill.py @@ -18,9 +18,7 @@ from abc import ABCMeta from os.path import join, exists -from ovos_config.locations import get_xdg_config_save_path from ovos_utils.log import LOG - from ovos_workshop.skills.base import BaseSkill, is_classic_core @@ -30,16 +28,9 @@ class _SkillMetaclass(ABCMeta): def __instancecheck__(self, instance): if is_classic_core(): # instance imported from vanilla mycroft - try: - from mycroft.skills import MycroftSkill as _CoreSkill - if issubclass(self.__class__, _CoreSkill): - return True - except ImportError: - # not running in core - standalone skill - pass - - # instance imported from workshop - # we can not patch mycroft-core class to make isinstance return True + from mycroft.skills import MycroftSkill as _CoreSkill + if issubclass(self.__class__, _CoreSkill): + return True return super().__instancecheck__(instance) @@ -75,22 +66,25 @@ def __init__(self, name=None, bus=None, use_settings=True, *args, **kwargs): self.settings_write_path = self.root_dir def _init_settings_manager(self): - try: - from mycroft.skills.settings import SkillSettingsManager - from mycroft.deprecated.skills.settings import SettingsMetaUploader - self.settings_manager = SkillSettingsManager(self) - # backwards compat - self.settings_meta has been deprecated in favor of settings manager - self._settings_meta = SettingsMetaUploader(self.root_dir, self.skill_id) - except ImportError: - pass + super()._init_settings_manager() + # backwards compat - self.settings_meta has been deprecated in favor of settings manager + if is_classic_core(): + from mycroft.skills.settings import SettingsMetaUploader + else: + try: # ovos-core compat layer + from mycroft.deprecated.skills.settings import SettingsMetaUploader + self._settings_meta = SettingsMetaUploader(self.root_dir, self.skill_id) + except ImportError: + pass # standalone skill, skip backwards compat property def _init_settings(self): """Setup skill settings.""" - # migrate settings if needed - if not exists(self._settings_path) and exists(self._old_settings_path): - LOG.warning("Found skill settings at pre-xdg location, migrating!") - shutil.copy(self._old_settings_path, self._settings_path) - LOG.info(f"{self._old_settings_path} moved to {self._settings_path}") + if is_classic_core(): + # migrate settings if needed + if not exists(self._settings_path) and exists(self._old_settings_path): + LOG.warning("Found skill settings at pre-xdg location, migrating!") + shutil.copy(self._old_settings_path, self._settings_path) + LOG.info(f"{self._old_settings_path} moved to {self._settings_path}") super()._init_settings() @@ -181,8 +175,9 @@ def _old_settings_path(self): # patched due to functional (internal) differences under mycroft-core @property def _settings_path(self): - if self.settings_write_path and self.settings_write_path != self.root_dir: - LOG.warning("self.settings_write_path has been deprecated! " - "Support will be dropped in a future release") - return join(self.settings_write_path, 'settings.json') + if is_classic_core(): + if self.settings_write_path and self.settings_write_path != self.root_dir: + LOG.warning("self.settings_write_path has been deprecated! " + "Support will be dropped in a future release") + return join(self.settings_write_path, 'settings.json') return super()._settings_path diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 80817457..eb426821 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -26,12 +26,13 @@ class OVOSSkill(MycroftSkill): """ def __init__(self, *args, **kwargs): - super(OVOSSkill, self).__init__(*args, **kwargs) + # note - define these before super() because of self.bind() self.private_settings = None self._threads = [] self._original_converse = self.converse self.intent_layers = IntentLayers() self.audio_service = None + super(OVOSSkill, self).__init__(*args, **kwargs) def bind(self, bus): super().bind(bus) diff --git a/test/unittests/ovos_tskill_abort/__init__.py b/test/unittests/ovos_tskill_abort/__init__.py index ba8d0456..46e54b6b 100644 --- a/test/unittests/ovos_tskill_abort/__init__.py +++ b/test/unittests/ovos_tskill_abort/__init__.py @@ -1,6 +1,6 @@ from ovos_workshop.decorators import killable_intent from ovos_workshop.skills.ovos import OVOSSkill -from mycroft.skills import intent_file_handler +from ovos_workshop.decorators import intent_file_handler from time import sleep diff --git a/test/unittests/skills/mocks.py b/test/unittests/skills/mocks.py index 6b3ff1f9..2d292975 100644 --- a/test/unittests/skills/mocks.py +++ b/test/unittests/skills/mocks.py @@ -15,7 +15,7 @@ from copy import deepcopy from unittest.mock import Mock -from mycroft.configuration.config import LocalConf, DEFAULT_CONFIG +from ovos_config import LocalConf, DEFAULT_CONFIG __CONFIG = LocalConf(DEFAULT_CONFIG) diff --git a/test/unittests/skills/test_skill/__init__.py b/test/unittests/skills/test_skill/__init__.py index e31f4997..2dff65b1 100644 --- a/test/unittests/skills/test_skill/__init__.py +++ b/test/unittests/skills/test_skill/__init__.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from mycroft.skills.core import MycroftSkill +from ovos_workshop.skills import MycroftSkill class LoadTestSkill(MycroftSkill): diff --git a/test/unittests/test_abort.py b/test/unittests/test_abort.py index b1fc7fce..2f058943 100644 --- a/test/unittests/test_abort.py +++ b/test/unittests/test_abort.py @@ -3,7 +3,7 @@ from os.path import dirname from time import sleep -from mycroft.skills.skill_loader import SkillLoader +from ovos_workshop.skill_launcher import SkillLoader from ovos_utils.messagebus import FakeBus, Message from ovos_workshop.skills.mycroft_skill import is_classic_core diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index 996ceb6d..801596e6 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -9,7 +9,7 @@ from mycroft.skills import MycroftSkill as CoreSkill from ovos_utils.messagebus import FakeBus from os.path import dirname -from mycroft.skills.skill_loader import SkillLoader +from ovos_workshop.skill_launcher import SkillLoader class TestSkill(unittest.TestCase): diff --git a/test/unittests/test_skill_classes.py b/test/unittests/test_skill_classes.py index 15eb0164..4a2361ea 100644 --- a/test/unittests/test_skill_classes.py +++ b/test/unittests/test_skill_classes.py @@ -6,6 +6,8 @@ from ovos_workshop.skills.ovos import OVOSSkill from ovos_utils.process_utils import RuntimeRequirements from ovos_workshop.skills.mycroft_skill import is_classic_core +from ovos_utils.messagebus import FakeBus +from ovos_workshop.settings import SkillSettingsManager class OfflineSkill(OVOSSkill): @@ -44,13 +46,11 @@ def __init__(self, *args, **kwargs): class TestSkills(unittest.TestCase): def test_settings_manager_init(self): - from ovos_utils.messagebus import FakeBus bus = FakeBus() skill_default = TestSkill(bus=bus) skill_default._startup(bus) # This doesn't apply to `mycroft-core`, only `ovos-core` if not is_classic_core(): - from mycroft.skills.settings import SkillSettingsManager self.assertIsInstance(skill_default.settings_manager, SkillSettingsManager) skill_disabled_settings = TestSkill(bus=bus, From 772d94f2869d5deaa63d95c87f9a204e55fec687 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 03:24:26 +0000 Subject: [PATCH 035/154] Increment Version to 0.0.12a13 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index f93e52c3..0ef0665d 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 12 +VERSION_ALPHA = 13 # END_VERSION_BLOCK From dce2695af531e04b0364412ffddd116ea33806bb Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 03:24:55 +0000 Subject: [PATCH 036/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bbb4859..8f0f8ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a12) (2023-04-22) +## [0.0.12a13](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a13) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a11...0.0.12a12) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a12...0.0.12a13) + +**Fixed bugs:** + +- fix/is\_classic\_core2 [\#76](https://github.com/OpenVoiceOS/OVOS-workshop/pull/76) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a12) (2023-04-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a11...V0.0.12a12) **Fixed bugs:** From 6b7fc3ccf579f9f9f8d6f3cee3257377652ce65c Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 22 Apr 2023 04:50:22 +0100 Subject: [PATCH 037/154] refactor/common_qa_speak (#63) --- ovos_workshop/skills/common_query_skill.py | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ovos_workshop/skills/common_query_skill.py b/ovos_workshop/skills/common_query_skill.py index affbaab3..1bb4fa2f 100644 --- a/ovos_workshop/skills/common_query_skill.py +++ b/ovos_workshop/skills/common_query_skill.py @@ -17,7 +17,7 @@ from ovos_utils.file_utils import resolve_resource_file from ovos_utils.log import LOG -from ovos_workshop.skills.ovos import OVOSSkill +from ovos_workshop.skills.ovos import OVOSSkill, is_classic_core class CQSMatchLevel(IntEnum): @@ -112,11 +112,13 @@ def __handle_question_query(self, message): match = result[0] level = result[1] answer = result[2] - callback = result[3] if len(result) > 3 else None + callback = result[3] if len(result) > 3 else {} confidence = self.__calc_confidence(match, search_phrase, level, answer) + callback["answer"] = answer # ensure we get it back in CQS_action self.bus.emit(message.response({"phrase": search_phrase, "skill_id": self.skill_id, "answer": answer, + "handles_speech": True, # signal we performed speech in the skill "callback_data": callback, "conf": confidence})) else: @@ -192,7 +194,22 @@ def __handle_query_action(self, message): # Not for this skill! return phrase = message.data["phrase"] - data = message.data.get("callback_data") + data = message.data.get("callback_data") or {} + if data.get("answer"): + # check core version, ovos-core does this speak call itself up to version 0.0.8a4 + core_speak = is_classic_core() + if not core_speak: + try: + from mycroft.version import OVOS_VERSION_MAJOR, OVOS_VERSION_MINOR, OVOS_VERSION_BUILD, OVOS_VERSIOM_ALPHA + if OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD < 8: + core_speak = True + elif OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD == 8 and \ + OVOS_VERSIOM_ALPHA < 5: + core_speak = True + except ImportError: + pass + if not core_speak: + self.speak(data["answer"]) # Invoke derived class to provide playback data self.CQS_action(phrase, data) From bf6e9fde7ee202550ae373ed4ba2b541aae313af Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 03:50:37 +0000 Subject: [PATCH 038/154] Increment Version to 0.0.12a14 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 0ef0665d..01ea6a89 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 13 +VERSION_ALPHA = 14 # END_VERSION_BLOCK From 91302535aeb4bf473dcaf36036ab4e94fe30c333 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 03:51:10 +0000 Subject: [PATCH 039/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f0f8ca2..808fe5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a13](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a13) (2023-04-22) +## [0.0.12a14](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a14) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a12...0.0.12a13) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a13...0.0.12a14) + +**Merged pull requests:** + +- refactor/common\_qa\_speak [\#63](https://github.com/OpenVoiceOS/OVOS-workshop/pull/63) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a13](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a13) (2023-04-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a12...V0.0.12a13) **Fixed bugs:** From 45ea6eadbdba1c395b2a83f4f393feaefe5f91b0 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 22 Apr 2023 05:59:51 +0100 Subject: [PATCH 040/154] python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11"] (#77) --- .github/workflows/build_tests.yml | 2 +- .github/workflows/unit_tests.yml | 2 +- ovos_workshop/skill_launcher.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index af72644c..9487491b 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -24,7 +24,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10" ] + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index eccc5c24..7ba6a12f 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -35,7 +35,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: [ 3.8, 3.9, "3.10", "3.11" ] + python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 2954cdc9..3cc5f92b 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -530,8 +530,8 @@ def launch_standalone_skill(skill_directory, skill_id): def _launch_script(): """USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]""" - - if (args_count := len(sys.argv)) == 2: + args_count = len(sys.argv) + if args_count == 2: skill_id = sys.argv[1] # preference to local skills From ee9c10f9ea671e47acecb616cdd3eb995f8af08c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 05:00:09 +0000 Subject: [PATCH 041/154] Increment Version to 0.0.12a15 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 01ea6a89..0be2bfd7 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 14 +VERSION_ALPHA = 15 # END_VERSION_BLOCK From c8dc26fbb4e864ddde681db6fc5279ff30544007 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 05:00:43 +0000 Subject: [PATCH 042/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808fe5f9..c7640adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a14](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a14) (2023-04-22) +## [0.0.12a15](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a15) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a13...0.0.12a14) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a14...0.0.12a15) + +**Merged pull requests:** + +- python-version: \[ 3.7, 3.8, 3.9, "3.10", "3.11"\] [\#77](https://github.com/OpenVoiceOS/OVOS-workshop/pull/77) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a14](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a14) (2023-04-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a13...V0.0.12a14) **Merged pull requests:** From aa973f795fc0fee62bd985f333032393a34bf81d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 23 Apr 2023 00:32:43 +0100 Subject: [PATCH 043/154] feat/standalone_skills (#78) --- ovos_workshop/skill_launcher.py | 125 ++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 3cc5f92b..f5ce2067 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -288,6 +288,7 @@ def reload_allowed(self): return self.active and (self.instance is None or self.instance.reload_skill) def reload(self): + self.load_attempted = True LOG.info(f'ATTEMPTING TO RELOAD SKILL: {self.skill_id}') if self.instance: if not self.instance.reload_skill: @@ -474,58 +475,80 @@ def load(self, skill_class): return self.loaded -def _connect_to_core(): - setup_locale() # ensure any initializations and resource loading is handled - bus = MessageBusClient() - bus.run_in_thread() - bus.connected_event.wait() - connected = False - while not connected: +class SkillContainer: + def __init__(self, skill_id, skill_directory=None, bus=None): + setup_locale() # ensure any initializations and resource loading is handled + self.bus = bus + self.skill_id = skill_id + if not skill_directory: # preference to local skills instead of plugins + for p in get_skill_directories(): + if isdir(f"{p}/{skill_id}"): + skill_directory = f"{p}/{skill_id}" + LOG.debug(f"found local skill {skill_id}: {skill_directory}") + break + self.skill_directory = skill_directory + self.skill_loader = None + + def _connect_to_core(self): + + if not self.bus: + self.bus = MessageBusClient() + self.bus.run_in_thread() + self.bus.connected_event.wait() + LOG.debug("checking skills service status") - response = bus.wait_for_response(Message(f'mycroft.skills.is_ready', + response = self.bus.wait_for_response(Message(f'mycroft.skills.is_ready', context={"source": "workshop", "destination": "skills"})) if response and response.data['status']: - connected = True + LOG.info("connected to core") + self.load_skill() else: LOG.warning("ovos-core does not seem to be running") - LOG.debug("connected to core") - return bus - - -def launch_plugin_skill(skill_id): - """ run a plugin skill standalone """ - bus = _connect_to_core() + self.bus.on("mycroft.ready", self.load_skill) - plugins = find_skill_plugins() - if skill_id not in plugins: - raise ValueError(f"unknown skill_id: {skill_id}") - skill_plugin = plugins[skill_id] - skill_loader = PluginSkillLoader(bus, skill_id) - try: - skill_loader.load(skill_plugin) - wait_for_exit_signal() - except KeyboardInterrupt: - skill_loader.deactivate() - except Exception: - LOG.exception(f'Load of skill {skill_id} failed!') - - -def launch_standalone_skill(skill_directory, skill_id): - """ run a skill standalone from a directory """ + def load_skill(self, message=None): + if self.skill_loader: + LOG.info("detected core reload, reloading skill") + self.skill_loader.reload() + return + LOG.info("launching skill") + if not self.skill_directory: + self._launch_plugin_skill() + else: + self._launch_standalone_skill() - bus = _connect_to_core() + def run(self): + self._connect_to_core() + try: + wait_for_exit_signal() + except KeyboardInterrupt: + pass + if self.skill_loader: + self.skill_loader.deactivate() + + def _launch_plugin_skill(self): + """ run a plugin skill standalone """ + + plugins = find_skill_plugins() + if self.skill_id not in plugins: + raise ValueError(f"unknown skill_id: {self.skill_id}") + skill_plugin = plugins[self.skill_id] + self.skill_loader = PluginSkillLoader(self.bus, self.skill_id) + try: + self.skill_loader.load(skill_plugin) + except Exception: + LOG.exception(f'Load of skill {self.skill_id} failed!') - skill_loader = SkillLoader(bus, skill_directory, - skill_id=skill_id) - try: - skill_loader.load() - wait_for_exit_signal() - except KeyboardInterrupt: - skill_loader.deactivate() - except Exception: - LOG.exception(f'Load of skill {skill_directory} failed!') + def _launch_standalone_skill(self): + """ run a skill standalone from a directory """ + self.skill_loader = SkillLoader(self.bus, self.skill_directory, + skill_id=self.skill_id) + try: + self.skill_loader.load() + except Exception: + LOG.exception(f'Load of skill {self.skill_directory} failed!') def _launch_script(): @@ -533,24 +556,16 @@ def _launch_script(): args_count = len(sys.argv) if args_count == 2: skill_id = sys.argv[1] - - # preference to local skills - for p in get_skill_directories(): - if isdir(f"{p}/{skill_id}"): - skill_directory = f"{p}/{skill_id}" - LOG.info(f"found local skill, loading {skill_directory}") - launch_standalone_skill(skill_directory, skill_id) - break - else: # plugin skill - LOG.info(f"found plugin skill {skill_id}") - launch_plugin_skill(skill_id) - + skill = SkillContainer(skill_id) elif args_count == 3: # user asked explicitly for a directory skill_id = sys.argv[1] skill_directory = sys.argv[2] - launch_standalone_skill(skill_directory, skill_id) + skill = SkillContainer(skill_id, skill_directory) else: print("USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]") raise SystemExit(2) + skill.run() + + From 92aea460f42457ef8ae39b59e3c3974c422f0720 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 23:33:03 +0000 Subject: [PATCH 044/154] Increment Version to 0.0.12a16 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 0be2bfd7..43ead864 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 15 +VERSION_ALPHA = 16 # END_VERSION_BLOCK From d87602ba6a5660acee9a29f23891d482c50eb5fa Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 22 Apr 2023 23:33:31 +0000 Subject: [PATCH 045/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7640adb..38ee25e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a15](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a15) (2023-04-22) +## [0.0.12a16](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a16) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a14...0.0.12a15) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a15...0.0.12a16) + +**Implemented enhancements:** + +- feat/standalone\_skills [\#78](https://github.com/OpenVoiceOS/OVOS-workshop/pull/78) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a15](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a15) (2023-04-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a14...V0.0.12a15) **Merged pull requests:** From 038d4239cc7edfca94a20beffd75501bf8280ce3 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 24 Apr 2023 09:52:29 +0100 Subject: [PATCH 046/154] hotfix/dependency_resolution ovos-utils[extras] caused issues on pip dependency resolution --- requirements/requirements.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c01def64..1f770113 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,5 @@ -ovos-utils[extras] < 0.1.0, >=0.0.31 -ovos_config~=0.0,>=0.0.4 +ovos-utils < 0.1.0, >=0.0.31 +ovos_config < 0.1.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 -ovos-bus-client < 0.1.0, >=0.0.3 \ No newline at end of file +ovos-bus-client < 0.1.0, >=0.0.3 +rapidfuzz \ No newline at end of file From 69cd7431e2f288913b044e20385d3fa487c1a8dc Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 24 Apr 2023 08:52:49 +0000 Subject: [PATCH 047/154] Increment Version to 0.0.12a17 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 43ead864..6b35a3d4 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 16 +VERSION_ALPHA = 17 # END_VERSION_BLOCK From 179be60be3a5128c06a21c52a3fa733174a643f8 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 24 Apr 2023 08:53:17 +0000 Subject: [PATCH 048/154] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ee25e6..e512a500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.0.12a16](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a16) (2023-04-22) +## [V0.0.12a16](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a16) (2023-04-22) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a15...0.0.12a16) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a15...V0.0.12a16) **Implemented enhancements:** From ae7d93d86f3e1254588d36d14eab722825f13dec Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 25 Apr 2023 18:42:10 +0100 Subject: [PATCH 049/154] refactor/fallback_skills_v2 (#66) --- ovos_workshop/skills/fallback.py | 194 ++++++++++++++++++- test/unittests/skills/test_fallback_skill.py | 2 +- test/unittests/test_skill_classes.py | 22 ++- 3 files changed, 212 insertions(+), 6 deletions(-) diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 23dda3b5..eb4bcdca 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -18,14 +18,52 @@ import operator from ovos_utils.log import LOG -from ovos_utils.messagebus import get_handler_name +from ovos_utils.messagebus import get_handler_name, Message from ovos_utils.metrics import Stopwatch from ovos_utils.skills import get_non_properties -from ovos_workshop.skills.ovos import OVOSSkill + from ovos_workshop.permissions import FallbackMode +from ovos_workshop.skills.ovos import OVOSSkill, is_classic_core + + +class _MutableFallback(type(OVOSSkill)): + """ To override isinstance checks we need to use a metaclass """ + + def __instancecheck__(self, instance): + if isinstance(instance, (FallbackSkillV1, FallbackSkillV2)): + return True + return super().__instancecheck__(instance) + +class FallbackSkill(OVOSSkill, metaclass=_MutableFallback): + def __new__(cls, *args, **kwargs): + is_old = is_classic_core() + if not is_old: + try: + from mycroft.version import OVOS_VERSION_MAJOR, OVOS_VERSION_MINOR, OVOS_VERSION_BUILD, OVOS_VERSION_ALPHA + if OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD < 8: + is_old = True + elif OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD == 8 \ + and 0 < OVOS_VERSION_ALPHA < 5: + is_old = True + except ImportError: + pass + if is_old: + return FallbackSkillV1(*args, **kwargs) + # core supports fallback V2 + return FallbackSkillV2(*args, **kwargs) -class FallbackSkill(OVOSSkill): + +class _MutableFallback1(type(OVOSSkill)): + """ To override isinstance checks we need to use a metaclass """ + + def __instancecheck__(self, instance): + if isinstance(instance, (FallbackSkillV2, FallbackSkill)): + return True + return super().__instancecheck__(instance) + + +class FallbackSkillV1(OVOSSkill, metaclass=_MutableFallback1): """Fallbacks come into play when no skill matches an Adapt or closely with a Padatious intent. All Fallback skills work together to give them a view of the user's utterance. Fallback handlers are called in an order @@ -233,7 +271,155 @@ def remove_instance_handlers(self): def default_shutdown(self): """Remove all registered handlers and perform skill shutdown.""" self.remove_instance_handlers() - super(FallbackSkill, self).default_shutdown() + super().default_shutdown() + + def _register_decorated(self): + """Register all intent handlers that are decorated with an intent. + + Looks for all functions that have been marked by a decorator + and read the intent data from them. The intent handlers aren't the + only decorators used. Skip properties as calling getattr on them + executes the code which may have unintended side-effects + """ + super()._register_decorated() + for attr_name in get_non_properties(self): + method = getattr(self, attr_name) + if hasattr(method, 'fallback_priority'): + self.register_fallback(method, method.fallback_priority) + + +class _MutableFallback2(type(OVOSSkill)): + """ To override isinstance checks we need to use a metaclass """ + + def __instancecheck__(self, instance): + if isinstance(instance, (FallbackSkillV1, FallbackSkill)): + return True + return super().__instancecheck__(instance) + + +class FallbackSkillV2(OVOSSkill, metaclass=_MutableFallback2): + """ + Fallbacks come into play when no skill matches an intent. + + Fallback handlers are called in an order determined the + priority provided when the skill is registered. + + ======== ======== ================================================ + Priority Who? Purpose + ======== ======== ================================================ + 1-4 RESERVED Unused for now, slot for pre-Padatious if needed + 5 MYCROFT Padatious near match (conf > 0.8) + 6-88 USER General + 89 MYCROFT Padatious loose match (conf > 0.5) + 90-99 USER Uncaught intents + 100+ MYCROFT Fallback Unknown or other future use + ======== ======== ================================================ + + Handlers with the numerically lowest priority are invoked first. + Multiple fallbacks can exist at the same priority, but no order is + guaranteed. + + A Fallback can either observe or consume an utterance. A consumed + utterance will not be see by any other Fallback handlers. + + A skill might register several handlers, the lowest priority will be reported to core + If a skill is selected by core then all handlers are checked by + their priority until one can handle the utterance + + A skill may return False in the can_answer method to request + that core does not execute it's fallback handlers + """ + + def __init__(self, bus=None, skill_id=""): + super().__init__(bus=bus, skill_id=skill_id) + # "skill_id": priority (int) overrides + self.fallback_config = self.config_core["skills"].get("fallbacks", {}) + self._fallback_handlers = [] + + @property + def priority(self): + priority_overrides = self.fallback_config.get("fallback_priorities", {}) + if self.skill_id in priority_overrides: + return priority_overrides.get(self.skill_id) + if len(self._fallback_handlers): + return min([p[0] for p in self._fallback_handlers]) + return 101 + + def can_answer(self, utterances, lang): + """Check if the skill can answer the particular question. + + + Arguments: + utterances (list): list of possible transcriptions to parse + lang (str) : lang code + Returns: + (bool) True if skill can handle the query + """ + return len(self._fallback_handlers) > 0 + + def _register_system_event_handlers(self): + """Add all events allowing the standard interaction with the Mycroft + system. + """ + super()._register_system_event_handlers() + self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, speak_errors=False) + self.add_event("ovos.skills.fallback.request", self._handle_fallback_request, speak_errors=False) + self.bus.emit(Message("ovos.skills.fallback.register", + {"skill_id": self.skill_id, "priority": self.priority})) + + def _handle_fallback_ack(self, message): + """Inform skills service we can handle fallbacks.""" + utts = message.data.get("utterances", []) + lang = message.data.get("lang") + self.bus.emit(message.reply( + "ovos.skills.fallback.pong", + data={"skill_id": self.skill_id, + "can_handle": self.can_answer(utts, lang)}, + context={"skill_id": self.skill_id})) + + def _handle_fallback_request(self, message): + # indicate fallback handling start + self.bus.emit(message.forward(f"ovos.skills.fallback.{self.skill_id}.start")) + + handler_name = None + + # each skill can register multiple handlers with different priorities + sorted_handlers = sorted(self._fallback_handlers, key=operator.itemgetter(0)) + for prio, handler in sorted_handlers: + try: + if handler(message): + # indicate completion + status = True + handler_name = get_handler_name(handler) + break + except Exception: + LOG.exception('Exception in fallback.') + else: + status = False + + self.bus.emit(message.forward(f"ovos.skills.fallback.{self.skill_id}.response", + data={"result": status, + "fallback_handler": handler_name})) + + def register_fallback(self, handler, priority): + """Register a fallback with the list of fallback handlers and with the + list of handlers registered by this instance + """ + + def wrapper(*args, **kwargs): + if handler(*args, **kwargs): + self.activate() + return True + return False + + self._fallback_handlers.append((priority, wrapper)) + self.bus.on(f"ovos.skills.fallback.{self.skill_id}", wrapper) + + def default_shutdown(self): + """Remove all registered handlers and perform skill shutdown.""" + self.bus.emit(Message("ovos.skills.fallback.deregister", {"skill_id": self.skill_id})) + self.bus.remove_all_listeners(f"ovos.skills.fallback.{self.skill_id}") + super().default_shutdown() def _register_decorated(self): """Register all intent handlers that are decorated with an intent. diff --git a/test/unittests/skills/test_fallback_skill.py b/test/unittests/skills/test_fallback_skill.py index ddb434d1..ffa7450e 100644 --- a/test/unittests/skills/test_fallback_skill.py +++ b/test/unittests/skills/test_fallback_skill.py @@ -1,6 +1,6 @@ from unittest import TestCase, mock -from ovos_workshop.skills.fallback import FallbackSkill +from ovos_workshop.skills.fallback import FallbackSkillV1 as FallbackSkill def setup_fallback(fb_class): diff --git a/test/unittests/test_skill_classes.py b/test/unittests/test_skill_classes.py index 4a2361ea..4dec7d90 100644 --- a/test/unittests/test_skill_classes.py +++ b/test/unittests/test_skill_classes.py @@ -99,7 +99,7 @@ def test_class_inheritance(self): from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.mycroft_skill import MycroftSkill - from ovos_workshop.skills.fallback import FallbackSkill + from ovos_workshop.skills.fallback import FallbackSkill, FallbackSkillV2, FallbackSkillV1 from ovos_workshop.app import OVOSAbstractApplication skill = TestSkill() @@ -124,5 +124,25 @@ def test_class_inheritance(self): self.assertIsInstance(fallback, BaseSkill) self.assertIsInstance(fallback, OVOSSkill) self.assertIsInstance(fallback, MycroftSkill) + self.assertIsInstance(fallback, FallbackSkillV1) + self.assertIsInstance(fallback, FallbackSkillV2) + self.assertIsInstance(fallback, FallbackSkill) + self.assertNotIsInstance(fallback, OVOSAbstractApplication) + + fallback = FallbackSkillV1("test") + self.assertIsInstance(fallback, BaseSkill) + self.assertIsInstance(fallback, OVOSSkill) + self.assertIsInstance(fallback, MycroftSkill) + self.assertIsInstance(fallback, FallbackSkillV1) + self.assertIsInstance(fallback, FallbackSkillV2) + self.assertIsInstance(fallback, FallbackSkill) + self.assertNotIsInstance(fallback, OVOSAbstractApplication) + + fallback = FallbackSkillV2("test") + self.assertIsInstance(fallback, BaseSkill) + self.assertIsInstance(fallback, OVOSSkill) + self.assertIsInstance(fallback, MycroftSkill) + self.assertIsInstance(fallback, FallbackSkillV1) + self.assertIsInstance(fallback, FallbackSkillV2) self.assertIsInstance(fallback, FallbackSkill) self.assertNotIsInstance(fallback, OVOSAbstractApplication) From 89292c65f1fe0e08a82b718a2a8f07c66bbcdc7c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 25 Apr 2023 17:42:32 +0000 Subject: [PATCH 050/154] Increment Version to 0.0.12a18 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 6b35a3d4..6ff6c56f 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 17 +VERSION_ALPHA = 18 # END_VERSION_BLOCK From 62964834d206f4403cd4bb648f94d81c8716da6e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 25 Apr 2023 17:43:15 +0000 Subject: [PATCH 051/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e512a500..7a1bf7fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.0.12a18](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a18) (2023-04-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a17...0.0.12a18) + +**Implemented enhancements:** + +- refactor/fallback\_skills\_v2 [\#66](https://github.com/OpenVoiceOS/OVOS-workshop/pull/66) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a17](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a17) (2023-04-24) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a16...V0.0.12a17) + ## [V0.0.12a16](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a16) (2023-04-22) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a15...V0.0.12a16) From 4a0affc64f32b827f79731b75740bef61732fdb8 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 26 Apr 2023 01:14:05 +0100 Subject: [PATCH 052/154] fix/fallback (#80) --- ovos_workshop/skills/fallback.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index eb4bcdca..7f95185e 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -53,6 +53,11 @@ def __new__(cls, *args, **kwargs): # core supports fallback V2 return FallbackSkillV2(*args, **kwargs) + @classmethod + def make_intent_failure_handler(cls, bus): + """backwards compat, old version of ovos-core call this method to bind the bus to old class""" + return FallbackSkillV1.make_intent_failure_handler(bus) + class _MutableFallback1(type(OVOSSkill)): """ To override isinstance checks we need to use a metaclass """ @@ -330,6 +335,11 @@ class FallbackSkillV2(OVOSSkill, metaclass=_MutableFallback2): that core does not execute it's fallback handlers """ + @classmethod + def make_intent_failure_handler(cls, bus): + """backwards compat, old version of ovos-core call this method to bind the bus to old class""" + return FallbackSkillV1.make_intent_failure_handler(bus) + def __init__(self, bus=None, skill_id=""): super().__init__(bus=bus, skill_id=skill_id) # "skill_id": priority (int) overrides From d329f1d02d46b869e5466a3a3ee66be55b10d252 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 26 Apr 2023 00:14:27 +0000 Subject: [PATCH 053/154] Increment Version to 0.0.12a19 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 6ff6c56f..a5fac350 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 18 +VERSION_ALPHA = 19 # END_VERSION_BLOCK From 695f44954174981ff1c0f8375a60d95a5fbf0082 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 26 Apr 2023 00:15:01 +0000 Subject: [PATCH 054/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1bf7fc..b19c7897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a18](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a18) (2023-04-25) +## [0.0.12a19](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a19) (2023-04-26) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a17...0.0.12a18) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a18...0.0.12a19) + +**Fixed bugs:** + +- fix/fallback [\#80](https://github.com/OpenVoiceOS/OVOS-workshop/pull/80) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a18](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a18) (2023-04-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a17...V0.0.12a18) **Implemented enhancements:** From 65b95b23fb65eda84c78754919160f43e523bb51 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 26 Apr 2023 05:16:06 +0100 Subject: [PATCH 055/154] fix/fallback_some_more (#81) --- ovos_workshop/skills/fallback.py | 54 +++++++++++++++----------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 7f95185e..d801066d 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -21,7 +21,7 @@ from ovos_utils.messagebus import get_handler_name, Message from ovos_utils.metrics import Stopwatch from ovos_utils.skills import get_non_properties - +from ovos_config import Configuration from ovos_workshop.permissions import FallbackMode from ovos_workshop.skills.ovos import OVOSSkill, is_classic_core @@ -30,13 +30,22 @@ class _MutableFallback(type(OVOSSkill)): """ To override isinstance checks we need to use a metaclass """ def __instancecheck__(self, instance): - if isinstance(instance, (FallbackSkillV1, FallbackSkillV2)): + if isinstance(instance, _MetaFB): return True return super().__instancecheck__(instance) -class FallbackSkill(OVOSSkill, metaclass=_MutableFallback): +class _MetaFB(OVOSSkill): + pass + + +class FallbackSkill(_MetaFB, metaclass=_MutableFallback): def __new__(cls, *args, **kwargs): + if cls is FallbackSkill: + # direct instantiation of class, dynamic wizardry or unittests going on... + # return V2 as expected, V1 will eventually be dropped + return FallbackSkillV2(*args, **kwargs) + is_old = is_classic_core() if not is_old: try: @@ -49,9 +58,10 @@ def __new__(cls, *args, **kwargs): except ImportError: pass if is_old: - return FallbackSkillV1(*args, **kwargs) - # core supports fallback V2 - return FallbackSkillV2(*args, **kwargs) + cls.__bases__ = (FallbackSkillV1, _MetaFB) + else: + cls.__bases__ = (FallbackSkillV2, _MetaFB) + return super().__new__(cls, *args, **kwargs) @classmethod def make_intent_failure_handler(cls, bus): @@ -59,16 +69,8 @@ def make_intent_failure_handler(cls, bus): return FallbackSkillV1.make_intent_failure_handler(bus) -class _MutableFallback1(type(OVOSSkill)): - """ To override isinstance checks we need to use a metaclass """ - - def __instancecheck__(self, instance): - if isinstance(instance, (FallbackSkillV2, FallbackSkill)): - return True - return super().__instancecheck__(instance) - -class FallbackSkillV1(OVOSSkill, metaclass=_MutableFallback1): +class FallbackSkillV1(_MetaFB, metaclass=_MutableFallback): """Fallbacks come into play when no skill matches an Adapt or closely with a Padatious intent. All Fallback skills work together to give them a view of the user's utterance. Fallback handlers are called in an order @@ -293,16 +295,7 @@ def _register_decorated(self): self.register_fallback(method, method.fallback_priority) -class _MutableFallback2(type(OVOSSkill)): - """ To override isinstance checks we need to use a metaclass """ - - def __instancecheck__(self, instance): - if isinstance(instance, (FallbackSkillV1, FallbackSkill)): - return True - return super().__instancecheck__(instance) - - -class FallbackSkillV2(OVOSSkill, metaclass=_MutableFallback2): +class FallbackSkillV2(_MetaFB, metaclass=_MutableFallback): """ Fallbacks come into play when no skill matches an intent. @@ -335,16 +328,17 @@ class FallbackSkillV2(OVOSSkill, metaclass=_MutableFallback2): that core does not execute it's fallback handlers """ + # "skill_id": priority (int) overrides + fallback_config = Configuration().get("skills", {}).get("fallbacks", {}) + @classmethod def make_intent_failure_handler(cls, bus): """backwards compat, old version of ovos-core call this method to bind the bus to old class""" return FallbackSkillV1.make_intent_failure_handler(bus) def __init__(self, bus=None, skill_id=""): - super().__init__(bus=bus, skill_id=skill_id) - # "skill_id": priority (int) overrides - self.fallback_config = self.config_core["skills"].get("fallbacks", {}) self._fallback_handlers = [] + super().__init__(bus=bus, skill_id=skill_id) @property def priority(self): @@ -373,7 +367,7 @@ def _register_system_event_handlers(self): """ super()._register_system_event_handlers() self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, speak_errors=False) - self.add_event("ovos.skills.fallback.request", self._handle_fallback_request, speak_errors=False) + self.add_event(f"ovos.skills.fallback.{self.skill_id}.request", self._handle_fallback_request, speak_errors=False) self.bus.emit(Message("ovos.skills.fallback.register", {"skill_id": self.skill_id, "priority": self.priority})) @@ -416,6 +410,8 @@ def register_fallback(self, handler, priority): list of handlers registered by this instance """ + LOG.info(f"registering fallback handler -> ovos.skills.fallback.{self.skill_id}") + def wrapper(*args, **kwargs): if handler(*args, **kwargs): self.activate() From d14e08dc91607ded4da1b5f36bcf761145e00a85 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 26 Apr 2023 04:16:26 +0000 Subject: [PATCH 056/154] Increment Version to 0.0.12a20 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index a5fac350..7c6eb760 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 19 +VERSION_ALPHA = 20 # END_VERSION_BLOCK From 78327c9f8c76316d970f8865e204f76bf932a050 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 26 Apr 2023 04:17:00 +0000 Subject: [PATCH 057/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b19c7897..e36ae266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a19](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a19) (2023-04-26) +## [0.0.12a20](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a20) (2023-04-26) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a18...0.0.12a19) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a19...0.0.12a20) + +**Fixed bugs:** + +- fix/fallback\_some\_more [\#81](https://github.com/OpenVoiceOS/OVOS-workshop/pull/81) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a19](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a19) (2023-04-26) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a18...V0.0.12a19) **Fixed bugs:** From 052b3a79f7352e6d9864fdf366f7b7738c918f8c Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 30 Apr 2023 17:07:02 +0100 Subject: [PATCH 058/154] fix/super_fallbacks (#83) --- ovos_workshop/skills/fallback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index d801066d..5eab2be2 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -58,9 +58,9 @@ def __new__(cls, *args, **kwargs): except ImportError: pass if is_old: - cls.__bases__ = (FallbackSkillV1, _MetaFB) + cls.__bases__ = (FallbackSkillV1, FallbackSkill, _MetaFB) else: - cls.__bases__ = (FallbackSkillV2, _MetaFB) + cls.__bases__ = (FallbackSkillV2, FallbackSkill, _MetaFB) return super().__new__(cls, *args, **kwargs) @classmethod From b14246a56f8a19b2b710fa01981fba05700df2a6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 16:07:20 +0000 Subject: [PATCH 059/154] Increment Version to 0.0.12a21 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 7c6eb760..b8dd6055 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 20 +VERSION_ALPHA = 21 # END_VERSION_BLOCK From 04fa2e2fa01ed5e7ad18cbf4523cd3cf6a6b7cef Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 16:07:50 +0000 Subject: [PATCH 060/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36ae266..073b8524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a20](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a20) (2023-04-26) +## [0.0.12a21](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a21) (2023-04-30) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a19...0.0.12a20) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a20...0.0.12a21) + +**Fixed bugs:** + +- fix/super\_fallbacks [\#83](https://github.com/OpenVoiceOS/OVOS-workshop/pull/83) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a20](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a20) (2023-04-26) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a19...V0.0.12a20) **Fixed bugs:** From 1a1649cc091b6a7c6d2afaf67e923f5c181d417d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:31:59 +0100 Subject: [PATCH 061/154] fix/activate_plugin_skills (#84) --- ovos_workshop/skill_launcher.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index f5ce2067..726871a4 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -474,6 +474,10 @@ def load(self, skill_class): self._communicate_load_status() return self.loaded + def activate(self): + self.active = True + self.load(self._skill_class) + class SkillContainer: def __init__(self, skill_id, skill_directory=None, bus=None): From cea1b6daa433f59aaca1ab1f9cfdfe582095c892 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 21:32:19 +0000 Subject: [PATCH 062/154] Increment Version to 0.0.12a22 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index b8dd6055..b998758c 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 21 +VERSION_ALPHA = 22 # END_VERSION_BLOCK From baa16ba616994cf67a0f3f676b5c94c95df38ffd Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 21:32:57 +0000 Subject: [PATCH 063/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073b8524..48ecc42e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a21](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a21) (2023-04-30) +## [0.0.12a22](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a22) (2023-04-30) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a20...0.0.12a21) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a21...0.0.12a22) + +**Fixed bugs:** + +- fix/activate\_plugin\_skills [\#84](https://github.com/OpenVoiceOS/OVOS-workshop/pull/84) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a21](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a21) (2023-04-30) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a20...V0.0.12a21) **Fixed bugs:** From 06edf868fe080b9a5ddfcf00a42238d3545e370f Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:37:01 +0100 Subject: [PATCH 064/154] workflow/codecov (#85) --- .github/workflows/coverage.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..4ebe2015 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,29 @@ +name: Upload Code Coverage +on: + push: + branches: + - master + - dev + workflow_dispatch: + +jobs: + test: + title: "Running codecov test" + type: "freestyle" # Run any command + image: "node:14.19.0" # The image in which command will be executed + working_directory: "${{clone}}" # Running command where code cloned + commands: + - "npm install --save-dev jest" + - "npx jest --coverage" + stage: "test" + + upload_cov: + title: "Running coverage" + type: "freestyle" # Run any command + image: "node:14.19.0" # The image in which command will be executed + working_directory: "${{clone}}" # Running command where code cloned + commands: + - "ci_env=`curl -s https://codecov.io/env`" + - "npm install codecov -g" + - "codecov -t ${{CODECOV_TOKEN}} -f ./coverage/ovos_cov.xml" + stage: "upload" \ No newline at end of file From 25f2fe3ecce1ba4058c1d938308c7bf47615929b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 30 Apr 2023 22:42:51 +0100 Subject: [PATCH 065/154] workflow/codecov --- .github/workflows/coverage.yml | 47 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4ebe2015..42832f29 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,4 +1,4 @@ -name: Upload Code Coverage +name: Run UnitTests on: push: branches: @@ -7,23 +7,28 @@ on: workflow_dispatch: jobs: - test: - title: "Running codecov test" - type: "freestyle" # Run any command - image: "node:14.19.0" # The image in which command will be executed - working_directory: "${{clone}}" # Running command where code cloned - commands: - - "npm install --save-dev jest" - - "npx jest --coverage" - stage: "test" - - upload_cov: - title: "Running coverage" - type: "freestyle" # Run any command - image: "node:14.19.0" # The image in which command will be executed - working_directory: "${{clone}}" # Running command where code cloned - commands: - - "ci_env=`curl -s https://codecov.io/env`" - - "npm install codecov -g" - - "codecov -t ${{CODECOV_TOKEN}} -f ./coverage/ovos_cov.xml" - stage: "upload" \ No newline at end of file + run: + runs-on: ubuntu-latest + env: + PYTHON: '3.10' + steps: + - uses: actions/checkout@master + - name: Setup Python + uses: actions/setup-python@master + with: + python-version: 3.10 + - name: Generate coverage report + run: | + pip install pytest + pip install pytest-cov + pytest --cov=./test/unittests --cov-report=xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./coverage/reports/ + fail_ci_if_error: true + files: ./coverage.xml,!./cache + flags: unittests + name: codecov-umbrella + verbose: true \ No newline at end of file From a7ba73bf308430677fe28d1e89b3c51b146e2243 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 30 Apr 2023 22:43:41 +0100 Subject: [PATCH 066/154] workflow/codecov --- .github/workflows/coverage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 42832f29..8d9b7cae 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,13 +10,13 @@ jobs: run: runs-on: ubuntu-latest env: - PYTHON: '3.10' + PYTHON: '3.9' steps: - uses: actions/checkout@master - name: Setup Python uses: actions/setup-python@master with: - python-version: 3.10 + python-version: 3.9 - name: Generate coverage report run: | pip install pytest From 44552e62e0623461cb9d46713848bf03087ab457 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 30 Apr 2023 22:45:18 +0100 Subject: [PATCH 067/154] workflow/codecov --- .github/workflows/coverage.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 8d9b7cae..37324581 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,6 +17,14 @@ jobs: uses: actions/setup-python@master with: python-version: 3.9 + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev + python -m pip install build wheel + - name: Install repo + run: | + pip install . - name: Generate coverage report run: | pip install pytest From 63960c82114dbdf580de71b29a256595233ad86d Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 30 Apr 2023 22:49:19 +0100 Subject: [PATCH 068/154] fix/missing_requirement (#86) --- requirements/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 1f770113..2842fb31 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,4 +2,5 @@ ovos-utils < 0.1.0, >=0.0.31 ovos_config < 0.1.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.3 +ovos_backend_client< 0.1.0, >= 0.0.6 rapidfuzz \ No newline at end of file From 9b54f3d226adfbeb9d2bc0dd0b5bb84cd44c6139 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 21:49:33 +0000 Subject: [PATCH 069/154] Increment Version to 0.0.12a23 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index b998758c..dcfd1f6c 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 22 +VERSION_ALPHA = 23 # END_VERSION_BLOCK From a7cf8849c28a18230aab0e7cfa1fdf57f8ebce97 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sun, 30 Apr 2023 21:50:00 +0000 Subject: [PATCH 070/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ecc42e..6dc4c78e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a22](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a22) (2023-04-30) +## [0.0.12a23](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a23) (2023-04-30) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a21...0.0.12a22) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a22...0.0.12a23) + +**Fixed bugs:** + +- fix/missing\_requirement [\#86](https://github.com/OpenVoiceOS/OVOS-workshop/pull/86) ([JarbasAl](https://github.com/JarbasAl)) + +**Merged pull requests:** + +- workflow/codecov [\#85](https://github.com/OpenVoiceOS/OVOS-workshop/pull/85) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a22](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a22) (2023-04-30) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a21...V0.0.12a22) **Fixed bugs:** From bda0c093c880608d4520636285a3238269a90c1e Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 30 Apr 2023 23:48:08 +0100 Subject: [PATCH 071/154] fix/codecov automation (#87) --- .github/workflows/coverage.yml | 8 ++- test/unittests/skills/test_mycroft_skill.py | 77 ++++++++++++++++++--- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 37324581..39bbf2d3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,8 +1,7 @@ -name: Run UnitTests +name: Run CodeCov on: push: branches: - - master - dev workflow_dispatch: @@ -22,6 +21,11 @@ jobs: sudo apt-get update sudo apt install python3-dev python -m pip install build wheel + - name: Install test dependencies + run: | + sudo apt install libssl-dev libfann-dev portaudio19-dev libpulse-dev + pip install ovos-core[all] + pip install pytest pytest-timeout pytest-cov adapt-parser~=0.5 - name: Install repo run: | pip install . diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill.py index ba49bb9f..071beff2 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill.py @@ -16,15 +16,16 @@ import sys import unittest -import pytest - from datetime import datetime from os.path import join, dirname, abspath from unittest.mock import MagicMock, patch +import pytest from adapt.intent import IntentBuilder -from ovos_config.config import Configuration from ovos_bus_client import Message +from ovos_config.config import Configuration + +from ovos_workshop.decorators import intent_handler, resting_screen_handler, intent_file_handler from ovos_workshop.skills.mycroft_skill import MycroftSkill from .mocks import base_config @@ -59,6 +60,21 @@ def vocab_base_path(): return join(dirname(__file__), '..', 'vocab_test') +class TestFunction(unittest.TestCase): + def test_resting_screen_handler(self): + class T(MycroftSkill): + def __init__(self): + self.name = 'TestObject' + + @resting_screen_handler('humbug') + def f(self): + pass + + test_class = T() + self.assertTrue('resting_handler' in dir(test_class.f)) + self.assertEqual(test_class.f.resting_handler, 'humbug') + + class TestMycroftSkill(unittest.TestCase): emitter = MockEmitter() regex_path = abspath(join(dirname(__file__), '../regex_test')) @@ -212,13 +228,15 @@ def _test_intent_file(self, s): 'file_name': join(dirname(__file__), 'intent_file', 'vocab', 'en-us', 'test.intent'), 'lang': 'en-us', - 'name': str(s.skill_id) + ':test.intent' + 'name': str(s.skill_id) + ':test.intent', + 'samples': [] }, { 'file_name': join(dirname(__file__), 'intent_file', 'vocab', 'en-us', 'test_ent.entity'), 'lang': 'en-us', - 'name': str(s.skill_id) + ':test_ent_87af9db6c8402bcfaa8ebc719ae4427c' + 'name': str(s.skill_id) + ':test_ent_87af9db6c8402bcfaa8ebc719ae4427c', + 'samples': [] } ] self.check_register_object_file(expected_types, expected_results) @@ -234,7 +252,7 @@ def test_register_decorators(self): """ Test decorated intents """ path_orig = sys.path sys.path.append(abspath(dirname(__file__))) - SimpleSkill5 = __import__('decorator_test_skill').TestSkill + s = SimpleSkill5() s.res_dir = abspath(join(dirname(__file__), 'intent_file')) s._startup(self.emitter, "A") @@ -247,6 +265,7 @@ def test_register_decorators(self): 'file_name': join(dirname(__file__), 'intent_file', 'vocab', 'en-us', 'test.intent'), 'lang': 'en-us', + 'samples': [], 'name': str(s.skill_id) + ':test.intent'}] self.check_register_decorators(expected) @@ -437,14 +456,14 @@ def test_voc_match_exact(self): exact=True)) self.assertFalse(s.voc_match("would you please turn off the lights", "turn_off_test", exact=True)) - + def test_voc_list(self): s = SimpleSkill1() s.root_dir = abspath(dirname(__file__)) self.assertEqual(s._voc_list("turn_off_test"), ["turn off", "switch off"]) - cache_key = s.lang+"turn_off_test" + cache_key = s.lang + "turn_off_test" self.assertIn(cache_key, s._voc_cache) def test_translate_locations(self): @@ -511,6 +530,23 @@ def test_native_langs(self): s.config_core['secondary_langs'] = secondary +class TestIntentCollisions(unittest.TestCase): + def test_two_intents_with_same_name(self): + emitter = MockEmitter() + skill = SameIntentNameSkill() + skill.bind(emitter) + with self.assertRaises(ValueError): + skill.initialize() + + def test_two_anonymous_intent_decorators(self): + """Two anonymous intent handlers should be ok.""" + emitter = MockEmitter() + skill = SameAnonymousIntentDecoratorsSkill() + skill.bind(emitter) + skill._register_decorated() + self.assertEqual(len(skill.intent_service.registered_intents), 2) + + class _TestSkill(MycroftSkill): def __init__(self): super().__init__() @@ -579,6 +615,21 @@ def stop(self): pass +class SimpleSkill5(MycroftSkill): + """ Test skill for intent_handler decorator. """ + + @intent_handler(IntentBuilder('a').require('Keyword').build()) + def handler(self, message): + pass + + @intent_file_handler('test.intent') + def handler2(self, message): + pass + + def stop(self): + pass + + class SimpleSkill6(_TestSkill): """ Test skill for padatious intent """ skill_id = 'A' @@ -603,3 +654,13 @@ def initialize(self): def handler(self, message): pass + + +class SameAnonymousIntentDecoratorsSkill(_TestSkill): + """Test skill for duplicate anonymous intent handlers.""" + skill_id = 'A' + + @intent_handler(IntentBuilder('').require('Keyword')) + @intent_handler(IntentBuilder('').require('OtherKeyword')) + def handler(self, message): + pass From 8bff4ad1e771ac24fbd60e2a0f6140b63551cbc5 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 2 May 2023 00:42:31 +0100 Subject: [PATCH 072/154] refactor/skill_init_wizardry (#70) --- ovos_workshop/skill_launcher.py | 43 ++++++++++++++++----------------- ovos_workshop/skills/base.py | 37 ++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 726871a4..7da40e78 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -409,30 +409,29 @@ def _create_skill_instance(self, skill_module=None): """ skill_module = skill_module or self.skill_module try: - skill_creator = get_create_skill_function(skill_module) or \ - self.skill_class - - # create the skill - # if the signature supports skill_id and bus pass them - # to fully initialize the skill in 1 go + # in skill classes __new__ should fully create the skill object try: - # many skills do not expose this, if they don't allow bus/skill_id kwargs - # in __init__ we need to manually call _startup - self.instance = skill_creator(bus=self.bus, - skill_id=self.skill_id) - # skills will have bus and skill_id available as soon as they call super() - except: - self.instance = skill_creator() - - if hasattr(self.instance, "is_fully_initialized"): - LOG.warning(f"Deprecated skill signature! Skill class should be" - f" imported from `ovos_workshop.skills`") - is_initialized = self.instance.is_fully_initialized - else: - is_initialized = self.instance._is_fully_initialized - if not is_initialized: - # finish initialization of skill class + skill_class = get_skill_class(skill_module) + self.instance = skill_class(bus=self.bus, skill_id=self.skill_id) + except: # guess it wasnt subclassing from ovos_workshop (fail here ?) + + # attempt to use old style create_skill function entrypoint + skill_creator = get_create_skill_function(skill_module) or self.skill_class + + # if the signature supports skill_id and bus pass them to fully initialize the skill in 1 go + try: + # skills that do will have bus and skill_id available as soon as they call super() + self.instance = skill_creator(bus=self.bus, + skill_id=self.skill_id) + except: + # most old skills do not expose bus/skill_id kwargs + self.instance = skill_creator() + + # finish initialization of skill if we didn't manage to inject skill_id and bus kwargs + # these skills only have skill_id and bus available in initialize, not in __init__ + if not self.instance._is_fully_initialized: self.instance._startup(self.bus, self.skill_id) + except Exception as e: LOG.exception(f'Skill __init__ failed with {e}') self.instance = None diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index c942acad..5d5c3909 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -170,11 +170,44 @@ class BaseSkill: """Base class for mycroft skills providing common behaviour and parameters to all Skill implementations. This base class does not require `mycroft` to be importable - Args: - name (str): skill name + skill_launcher.py used to be skill_loader-py in mycroft-core + + for launching skills one can use skill_launcher.py to run them standalone (eg, docker), + but the main objective is to make skills work more like proper python objects and allow usage of the class directly + + the considerations are: + + - most skills in the wild dont expose kwargs, so dont accept skill_id or bus + - most skills expect a loader class to set up the bus and skill_id after object creation + - skills can not do pythonic things in init, instead of doing things after super() devs are expected to use initialize() which is a mycroft invention and non-standard + - main concern is that anything depending on self.skill_id being set can not be used in init method (eg. self.settings and self.file_system) + - __new__ uncouples the skill init from a helper class, making skills work like regular python objects + - the magic in `__new__` is just so we dont break everything in the wild, since we cant start requiring skill_id and bus args + + KwArgs: + name (str): skill name - DEPRECATED + skill_id (str): unique skill identifier bus (MycroftWebsocketClient): Optional bus connection """ + def __new__(cls, *args, **kwargs): + if "skill_id" in kwargs and "bus" in kwargs: + skill_id = kwargs["skill_id"] + bus = kwargs["bus"] + try: + # skill follows latest best practices, accepts kwargs and does its own init + return super().__new__(cls, skill_id=skill_id, bus=bus) + except: + # skill did not update its init method, let's do some magic to init it manually + skill = super().__new__(cls, *args, **kwargs) + skill._startup(bus, skill_id) + return skill + + # skill loader was not used to create skill object, we are missing the kwargs + # skill wont be fully inited, please move logic to initialize + LOG.warning(f"{cls.__name__} not fully inited, self.bus and self.skill_id will only be available in self.initialize") + return super().__new__(cls) + def __init__(self, name=None, bus=None, resources_dir=None, settings: JsonStorage = None, gui=None, enable_settings_manager=True, From 3408e5146d61dbba53aca4f6104c004514845f4b Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 1 May 2023 23:42:51 +0000 Subject: [PATCH 073/154] Increment Version to 0.0.12a24 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index dcfd1f6c..d39b7de4 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 23 +VERSION_ALPHA = 24 # END_VERSION_BLOCK From 398f96458f418fd4db9d07fcb4d0f383a4128fe2 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 1 May 2023 23:43:22 +0000 Subject: [PATCH 074/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc4c78e..ff9f899f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a23](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a23) (2023-04-30) +## [0.0.12a24](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a24) (2023-05-01) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a22...0.0.12a23) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a23...0.0.12a24) + +**Implemented enhancements:** + +- refactor/skill\_init\_wizardry [\#70](https://github.com/OpenVoiceOS/OVOS-workshop/pull/70) ([JarbasAl](https://github.com/JarbasAl)) + +**Merged pull requests:** + +- fix/codecov automation [\#87](https://github.com/OpenVoiceOS/OVOS-workshop/pull/87) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a23](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a23) (2023-04-30) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a22...V0.0.12a23) **Fixed bugs:** From 498e3730fb82a33a93bc1ea4ea1d905794f89420 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 3 May 2023 22:24:12 +0100 Subject: [PATCH 075/154] fix/core_reload in standalone launcher (#88) --- ovos_workshop/skill_launcher.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 7da40e78..776e312d 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -463,6 +463,9 @@ def __init__(self, bus, skill_id): def load(self, skill_class): LOG.info('ATTEMPTING TO LOAD PLUGIN SKILL: ' + self.skill_id) self._skill_class = skill_class + return self._load() + + def _load(self): self._prepare_for_load() if self.is_blacklisted: self._skip_load() From c9e4fe8ed313973f78773bcdb417470c8db292ae Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 3 May 2023 21:24:26 +0000 Subject: [PATCH 076/154] Increment Version to 0.0.12a25 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index d39b7de4..c75daa93 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 24 +VERSION_ALPHA = 25 # END_VERSION_BLOCK From 882319138769bff7441efa969bed637ad31b7f24 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 3 May 2023 21:24:54 +0000 Subject: [PATCH 077/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff9f899f..e5681b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a24](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a24) (2023-05-01) +## [0.0.12a25](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a25) (2023-05-03) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a23...0.0.12a24) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a24...0.0.12a25) + +**Fixed bugs:** + +- fix/core\_reload in standalone launcher [\#88](https://github.com/OpenVoiceOS/OVOS-workshop/pull/88) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a24](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a24) (2023-05-01) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a23...V0.0.12a24) **Implemented enhancements:** From 70bc851c88702f0350c57a7b75d57cb24ae37303 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 16 May 2023 19:48:54 -0700 Subject: [PATCH 078/154] Implement Unit Tests (#89) --- .github/workflows/build_tests.yml | 3 +- .github/workflows/unit_tests.yml | 1 - ovos_workshop/decorators/layers.py | 1 - ovos_workshop/resource_files.py | 203 +++++------ ovos_workshop/skill_launcher.py | 332 +++++++++++------- ovos_workshop/skills/base.py | 46 ++- requirements/requirements.txt | 2 +- .../{test_abort.py => test_decorators.py} | 20 ++ test/unittests/test_filesystem.py | 40 +++ test/unittests/test_permissions.py | 21 ++ test/unittests/test_resource_files.py | 106 ++++++ test/unittests/test_settings.py | 8 + test/unittests/test_skill_classes.py | 40 +-- test/unittests/test_skill_launcher.py | 141 ++++++++ 14 files changed, 680 insertions(+), 284 deletions(-) rename test/unittests/{test_abort.py => test_decorators.py} (97%) create mode 100644 test/unittests/test_filesystem.py create mode 100644 test/unittests/test_permissions.py create mode 100644 test/unittests/test_resource_files.py create mode 100644 test/unittests/test_settings.py create mode 100644 test/unittests/test_skill_launcher.py diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 9487491b..173d2d25 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -22,7 +22,6 @@ on: jobs: build_tests: strategy: - max-parallel: 2 matrix: python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] runs-on: ubuntu-latest @@ -48,7 +47,7 @@ jobs: - name: Install package run: | pip install .[all] - - uses: pypa/gh-action-pip-audit@v1.0.0 + - uses: pypa/gh-action-pip-audit@v1.0.7 with: # Ignore setuptools vulnerability we can't do much about ignore-vulns: | diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 7ba6a12f..eb8a113c 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -33,7 +33,6 @@ on: jobs: unit_tests: strategy: - max-parallel: 2 matrix: python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] runs-on: ubuntu-latest diff --git a/ovos_workshop/decorators/layers.py b/ovos_workshop/decorators/layers.py index b305fa78..e39ca520 100644 --- a/ovos_workshop/decorators/layers.py +++ b/ovos_workshop/decorators/layers.py @@ -3,7 +3,6 @@ from ovos_utils.log import LOG - def dig_for_skill(max_records: int = 10): from ovos_workshop.app import OVOSAbstractApplication from ovos_workshop.skills import MycroftSkill diff --git a/ovos_workshop/resource_files.py b/ovos_workshop/resource_files.py index 39700a80..5ecd1b83 100644 --- a/ovos_workshop/resource_files.py +++ b/ovos_workshop/resource_files.py @@ -47,9 +47,19 @@ ) -def locate_base_directories(skill_directory, resource_subdirectory=None): - base_dirs = [Path(skill_directory, resource_subdirectory)] if resource_subdirectory else [] - base_dirs += [Path(skill_directory, "locale"), Path(skill_directory, "text")] +def locate_base_directories(skill_directory: str, + resource_subdirectory: Optional[str] = None) -> \ + List[Path]: + """ + Locate all possible resource directories found in the given skill_directory + @param skill_directory: skill base directory to search for resources + @param resource_subdirectory: optional extra resource directory to prepend + @return: list of existing skill resource directories + """ + base_dirs = [Path(skill_directory, resource_subdirectory)] if \ + resource_subdirectory else [] + base_dirs += [Path(skill_directory, "locale"), + Path(skill_directory, "text")] candidates = [] for directory in base_dirs: if directory.exists(): @@ -57,7 +67,17 @@ def locate_base_directories(skill_directory, resource_subdirectory=None): return candidates -def locate_lang_directories(lang, skill_directory, resource_subdirectory=None): +def locate_lang_directories(lang: str, skill_directory: str, + resource_subdirectory: Optional[str] = None) -> \ + List[Path]: + """ + Locate all possible resource directories found in the given skill_directory + for the specified language + @param lang: BCP-47 language code to get resources for + @param skill_directory: skill base directory to search for resources + @param resource_subdirectory: optional extra resource directory to prepend + @return: list of existing skill resource directories for the given lang + """ base_lang = lang.split("-")[0] base_dirs = [Path(skill_directory, "locale"), Path(skill_directory, "text")] @@ -72,6 +92,77 @@ def locate_lang_directories(lang, skill_directory, resource_subdirectory=None): return candidates +def resolve_resource_file(res_name: str) -> Optional[str]: + """Convert a resource into an absolute filename. + + Resource names are in the form: 'filename.ext' + or 'path/filename.ext' + + The system wil look for $XDG_DATA_DIRS/mycroft/res_name first + (defaults to ~/.local/share/mycroft/res_name), and if not found will + look at /opt/mycroft/res_name, then finally it will look for res_name + in the 'mycroft/res' folder of the source code package. + + Example: + With mycroft running as the user 'bob', if you called + ``resolve_resource_file('snd/beep.wav')`` + it would return either: + '$XDG_DATA_DIRS/mycroft/beep.wav', + '/home/bob/.mycroft/snd/beep.wav' or + '/opt/mycroft/snd/beep.wav' or + '.../mycroft/res/snd/beep.wav' + where the '...' is replaced by the path + where the package has been installed. + + Args: + res_name (str): a resource path/name + + Returns: + (str) path to resource or None if no resource found + """ + # TODO: Deprecate in 0.1.0 + LOG.warning(f"This method has moved to `ovos_utils.file_utils` and will be" + f"removed in a future release.") + from ovos_utils.file_utils import resolve_resource_file + config = Configuration() + return resolve_resource_file(res_name, config=config) + + +def find_resource(res_name: str, root_dir: str, res_dirname: str, + lang: Optional[str] = None) -> Optional[Path]: + """ + Find a resource file. + + Searches for the given filename using this scheme: + 1. Search the resource lang directory: + /// + 2. Search the resource directory: + // + 3. Search the locale lang directory or other subdirectory: + /locale// or + /locale//.../ + + Args: + res_name (string): The resource name to be found + root_dir (string): A skill root directory + res_dirname (string): A skill sub directory + lang (string): language folder to be used + + Returns: + Path: The full path to the resource file or None if not found + """ + if lang: + for directory in locate_lang_directories(lang, root_dir, res_dirname): + for x in directory.iterdir(): + if x.is_file() and res_name == x.name: + return x + + for directory in locate_base_directories(root_dir, res_dirname): + for d, _, file_names in walk(directory): + if res_name in file_names: + return Path(directory, d, res_name) + + class ResourceType: """Defines the attributes of a type of skill resource. @@ -766,107 +857,3 @@ def _log_extraction_result(self, extract: str): LOG.info(f"No {self.group_name.lower()} extracted from utterance") else: LOG.info(f"{self.group_name} extracted from utterance: " + extract) - - -def resolve_resource_file(res_name): - """Convert a resource into an absolute filename. - - Resource names are in the form: 'filename.ext' - or 'path/filename.ext' - - The system wil look for $XDG_DATA_DIRS/mycroft/res_name first - (defaults to ~/.local/share/mycroft/res_name), and if not found will - look at /opt/mycroft/res_name, then finally it will look for res_name - in the 'mycroft/res' folder of the source code package. - - Example: - With mycroft running as the user 'bob', if you called - ``resolve_resource_file('snd/beep.wav')`` - it would return either: - '$XDG_DATA_DIRS/mycroft/beep.wav', - '/home/bob/.mycroft/snd/beep.wav' or - '/opt/mycroft/snd/beep.wav' or - '.../mycroft/res/snd/beep.wav' - where the '...' is replaced by the path - where the package has been installed. - - Args: - res_name (str): a resource path/name - - Returns: - (str) path to resource or None if no resource found - """ - config = Configuration() - - # First look for fully qualified file (e.g. a user setting) - if os.path.isfile(res_name): - return res_name - - # Now look for XDG_DATA_DIRS - for path in get_xdg_data_dirs(): - filename = os.path.join(path, res_name) - if os.path.isfile(filename): - return filename - - # Now look in the old user location - filename = os.path.join(os.path.expanduser('~'), - f'.{get_xdg_base()}', - res_name) - if os.path.isfile(filename): - return filename - - # Next look for /opt/mycroft/res/res_name - data_dir = config.get('data_dir', get_xdg_data_save_path()) - res_dir = os.path.join(data_dir, 'res') - filename = os.path.expanduser(os.path.join(res_dir, res_name)) - if os.path.isfile(filename): - return filename - - filename = f"{dirname(__file__)}/res" - if os.path.isfile(filename): - return filename - - # Finally look for it in the ovos-core package - try: - from mycroft import MYCROFT_ROOT_PATH - filename = f"{MYCROFT_ROOT_PATH}/mycroft/res/{res_name}" - filename = os.path.abspath(os.path.normpath(filename)) - if os.path.isfile(filename): - return filename - except ImportError: - pass - - return None # Resource cannot be resolved - - -def find_resource(res_name, root_dir, res_dirname, lang=None): - """Find a resource file. - - Searches for the given filename using this scheme: - 1. Search the resource lang directory: - /// - 2. Search the resource directory: - // - 3. Search the locale lang directory or other subdirectory: - /locale// or - /locale//.../ - - Args: - res_name (string): The resource name to be found - root_dir (string): A skill root directory - res_dirname (string): A skill sub directory - lang (string): language folder to be used - - Returns: - Path: The full path to the resource file or None if not found - """ - if lang: - for directory in locate_lang_directories(lang, root_dir, res_dirname): - for x in directory.iterdir(): - if x.is_file() and res_name == x.name: - return x - - for directory in locate_base_directories(root_dir, res_dirname): - for d, _, file_names in walk(directory): - if res_name in file_names: - return Path(directory, d, res_name) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 776e312d..7ea8203e 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -4,13 +4,14 @@ from os.path import isdir import sys from inspect import isclass -from os import path, makedirs +from types import ModuleType +from typing import Optional + from time import time from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_config.config import Configuration -from ovos_config.locations import get_xdg_data_dirs, get_xdg_data_save_path from ovos_config.locale import setup_locale from ovos_plugin_manager.skills import find_skill_plugins from ovos_utils import wait_for_exit_signal @@ -37,84 +38,24 @@ def get_skill_directories(conf=None): - """ returns list of skill directories ordered by expected loading order - - This corresponds to: - - XDG_DATA_DIRS - - user defined extra directories - - Each directory contains individual skill folders to be loaded - - If a skill exists in more than one directory (same folder name) previous instances will be ignored - ie. directories at the end of the list have priority over earlier directories - - NOTE: empty folders are interpreted as disabled skills - - new directories can be defined in mycroft.conf by specifying a full path - each extra directory is expected to contain individual skill folders to be loaded - - the xdg folder name can also be changed, it defaults to "skills" - eg. ~/.local/share/mycroft/FOLDER_NAME - - { - "skills": { - "directory": "skills", - "extra_directories": ["path/to/extra/dir/to/scan/for/skills"] - } - } - - Args: - conf (dict): mycroft.conf dict, will be loaded automatically if None - """ - # the contents of each skills directory must be individual skill folders - # we are still dependent on the mycroft-core structure of skill_id/__init__.py - + # TODO: Deprecate in 0.1.0 + LOG.warning(f"This method has moved to `ovos_utils.skills.locations` " + f"and will be removed in a future release.") + from ovos_utils.skills.locations import get_skill_directories conf = conf or Configuration() - folder = conf["skills"].get("directory") - - # load all valid XDG paths - # NOTE: skills are actually code, but treated as user data! - # they should be considered applets rather than full applications - skill_locations = list(reversed( - [os.path.join(p, folder) for p in get_xdg_data_dirs()] - )) + return get_skill_directories(conf) - # load additional explicitly configured directories - conf = conf.get("skills") or {} - # extra_directories is a list of directories containing skill subdirectories - # NOT a list of individual skill folders - skill_locations += conf.get("extra_directories") or [] - return skill_locations - -def get_default_skills_directory(): - """ return default directory to scan for skills - - data_dir is always XDG_DATA_DIR - If xdg is disabled then data_dir by default corresponds to /opt/mycroft - - users can define the data directory in mycroft.conf - the skills folder name (relative to data_dir) can also be defined there - - NOTE: folder name also impacts all XDG skill directories! - - { - "skills": { - "directory_override": "/opt/mycroft/hardcoded_path/skills" - } - } - - Args: - conf (dict): mycroft.conf dict, will be loaded automatically if None - """ - folder = Configuration()["skills"].get("directory") - skills_folder = os.path.join(get_xdg_data_save_path(), folder) - # create folder if needed - makedirs(skills_folder, exist_ok=True) - return path.expanduser(skills_folder) +def get_default_skills_directory(conf=None): + # TODO: Deprecate in 0.1.0 + LOG.warning(f"This method has moved to `ovos_utils.skills.locations` " + f"and will be removed in a future release.") + from ovos_utils.skills.locations import get_default_skills_directory + conf = conf or Configuration() + return get_default_skills_directory(conf) -def remove_submodule_refs(module_name): +def remove_submodule_refs(module_name: str): """Ensure submodules are reloaded by removing the refs from sys.modules. Python import system puts a reference for each module in the sys.modules @@ -136,7 +77,7 @@ def remove_submodule_refs(module_name): del sys.modules[m] -def load_skill_module(path, skill_id): +def load_skill_module(path: str, skill_id: str) -> ModuleType: """Load a skill module This function handles the differences between python 3.4 and 3.5+ as well @@ -145,6 +86,8 @@ def load_skill_module(path, skill_id): Args: path: Path to the skill main file (__init__.py) skill_id: skill_id used as skill identifier in the module list + Returns: + loaded skill module """ module_name = skill_id.replace('.', '_') @@ -157,7 +100,7 @@ def load_skill_module(path, skill_id): return mod -def get_skill_class(skill_module): +def get_skill_class(skill_module: ModuleType) -> Optional[callable]: """Find MycroftSkill based class in skill module. Arguments: @@ -166,6 +109,8 @@ def get_skill_class(skill_module): Returns: (MycroftSkill): Found subclass of MycroftSkill or None. """ + if not skill_module: + raise ValueError("Expected module and got None") if callable(skill_module): # it's a skill plugin # either a func that returns the skill or the skill class itself @@ -193,7 +138,7 @@ def get_skill_class(skill_module): return None -def get_create_skill_function(skill_module): +def get_create_skill_function(skill_module) -> Optional[callable]: """Find create_skill function in skill module. Arguments: @@ -224,26 +169,41 @@ def __init__(self, bus, skill_directory=None, skill_id=None): self.skill_module = None @property - def loaded(self): - return self._loaded # or self.instance is None + def loaded(self) -> bool: + """ + Return True if skill is loaded + """ + return self._loaded @loaded.setter - def loaded(self, val): + def loaded(self, val: bool): + """ + Set the skill as loaded + """ self._loaded = val @property - def skill_directory(self): + def skill_directory(self) -> Optional[str]: + """ + Return the skill directory or `None` if unset and no instance exists + """ skill_dir = self._skill_directory if self.instance and not skill_dir: skill_dir = self.instance.root_dir return skill_dir @skill_directory.setter - def skill_directory(self, val): + def skill_directory(self, val: str): + """ + Set (override) the skill ID + """ self._skill_directory = val @property - def skill_id(self): + def skill_id(self) -> Optional[str]: + """ + Return the skill's reported Skill ID + """ skill_id = self._skill_id if self.instance and not skill_id: skill_id = self.instance.skill_id @@ -252,11 +212,17 @@ def skill_id(self): return skill_id @skill_id.setter - def skill_id(self, val): + def skill_id(self, val: str): + """ + Set (override) the skill ID + """ self._skill_id = val @property - def skill_class(self): + def skill_class(self) -> Optional[callable]: + """ + Get the skill's class + """ skill_class = self._skill_class if self.instance and not skill_class: skill_class = self.instance.__class__ @@ -265,18 +231,27 @@ def skill_class(self): return skill_class @skill_class.setter - def skill_class(self, val): + def skill_class(self, val: callable): + """ + Set (override) the skill class + """ self._skill_class = val @property - def runtime_requirements(self): - if not self.skill_class: + def runtime_requirements(self) -> RuntimeRequirements: + """ + Return the skill's runtime requirements + """ + if not self.skill_class or not hasattr(self.skill_class, + "runtime_requirements"): return RuntimeRequirements() return self.skill_class.runtime_requirements @property - def is_blacklisted(self): - """Boolean value representing whether or not a skill is blacklisted.""" + def is_blacklisted(self) -> bool: + """ + Return true if the skill is blacklisted in configuration + """ blacklist = self.config['skills'].get('blacklisted_skills') or [] if self.skill_id in blacklist: return True @@ -284,10 +259,18 @@ def is_blacklisted(self): return False @property - def reload_allowed(self): - return self.active and (self.instance is None or self.instance.reload_skill) + def reload_allowed(self) -> bool: + """ + Return true if the skill can be reloaded + """ + return self.active and (self.instance is None or + self.instance.reload_skill) - def reload(self): + def reload(self) -> bool: + """ + Request reload the skill + @return: True if skill was reloaded + """ self.load_attempted = True LOG.info(f'ATTEMPTING TO RELOAD SKILL: {self.skill_id}') if self.instance: @@ -297,12 +280,18 @@ def reload(self): self._unload() return self._load() - def load(self): + def load(self, _=None) -> bool: + """ + Request to load the skill + @return: True if skill was loaded + """ LOG.info(f'ATTEMPTING TO LOAD SKILL: {self.skill_id}') return self._load() def _unload(self): - """Remove listeners and stop threads before loading""" + """ + Remove listeners and stop threads before loading + """ if self._watchdog: self._watchdog.shutdown() self._watchdog = None @@ -313,45 +302,67 @@ def _unload(self): self._emit_skill_shutdown_event() def unload(self): + """ + Shutdown and unload the skill instance + """ if self.instance: self._execute_instance_shutdown() def activate(self): + """ + Mark skill as active and (re)load the skill + """ self.active = True self.load() def deactivate(self): + """ + Mark skill as inactive and unload the skill + """ self.active = False self.unload() def _execute_instance_shutdown(self): - """Call the shutdown method of the skill being reloaded.""" + """ + Call the shutdown method of the skill being reloaded. + """ try: self.instance.default_shutdown() except Exception: - LOG.exception(f'An error occurred while shutting down {self.skill_id}') + LOG.exception(f'An error occurred while shutting down ' + f'{self.skill_id}') else: LOG.info(f'Skill {self.skill_id} shut down successfully') del self.instance self.instance = None def _garbage_collect(self): - """Invoke Python garbage collector to remove false references""" + """ + Invoke Python garbage collector to remove false references + """ gc.collect() # Remove two local references that are known refs = sys.getrefcount(self.instance) - 2 if refs > 0: LOG.warning( - f"After shutdown of {self.skill_id} there are still {refs} references " - "remaining. The skill won't be cleaned from memory." + f"After shutdown of {self.skill_id} there are still {refs} " + f"references remaining. The skill won't be cleaned from memory." ) def _emit_skill_shutdown_event(self): + """ + Emit `mycroft.skills.shutdown` to notify the skill is being shutdown + """ message = Message("mycroft.skills.shutdown", {"path": self.skill_directory, "id": self.skill_id}) self.bus.emit(message) - def _load(self): + def _load(self) -> bool: + """ + Load the skill if it is not blacklisted, emit load status, start file + watchers, and return load status. + @return: True if skill was loaded + """ self._prepare_for_load() if self.is_blacklisted: self._skip_load() @@ -365,28 +376,45 @@ def _load(self): return self.loaded def _start_filewatcher(self): + """ + Start a FileWatcher if one isn't already active + """ if not self._watchdog: self._watchdog = FileWatcher([self.skill_directory], callback=self._handle_filechange, recursive=True) def _handle_filechange(self): + """ + Handle a file change notification by reloading the skill + """ LOG.info("Skill change detected!") try: if self.reload_allowed: self.reload() except Exception: - LOG.exception(f'Unhandled exception occurred while reloading {self.skill_directory}') + LOG.exception(f'Unhandled exception occurred while reloading ' + f'{self.skill_directory}') def _prepare_for_load(self): + """ + Prepare SkillLoader for skill load + """ self.load_attempted = True self.instance = None def _skip_load(self): - LOG.info(f'Skill {self.skill_id} is blacklisted - it will not be loaded') + """ + Log a warning when requested skill load is skipped + """ + LOG.info(f'Skill {self.skill_id} is blacklisted - ' + f'it will not be loaded') - def _load_skill_source(self): - """Use Python's import library to load a skill's source code.""" + def _load_skill_source(self) -> ModuleType: + """ + Use Python's import library to load a skill's source code. + @return: Skill module to instantiate + """ main_file_path = os.path.join(self.skill_directory, SKILL_MAIN_MODULE) skill_module = None if not os.path.exists(main_file_path): @@ -398,8 +426,11 @@ def _load_skill_source(self): LOG.exception(f'Failed to load skill: {self.skill_id} ({e})') return skill_module - def _create_skill_instance(self, skill_module=None): - """create the skill object. + def _create_skill_instance(self, + skill_module: Optional[ModuleType] = None) -> \ + bool: + """ + Create the skill object. Arguments: skill_module (module): Module to load from @@ -408,30 +439,43 @@ def _create_skill_instance(self, skill_module=None): (bool): True if skill was loaded successfully. """ skill_module = skill_module or self.skill_module + try: # in skill classes __new__ should fully create the skill object - try: - skill_class = get_skill_class(skill_module) - self.instance = skill_class(bus=self.bus, skill_id=self.skill_id) - except: # guess it wasnt subclassing from ovos_workshop (fail here ?) - - # attempt to use old style create_skill function entrypoint - skill_creator = get_create_skill_function(skill_module) or self.skill_class - - # if the signature supports skill_id and bus pass them to fully initialize the skill in 1 go - try: - # skills that do will have bus and skill_id available as soon as they call super() - self.instance = skill_creator(bus=self.bus, - skill_id=self.skill_id) - except: - # most old skills do not expose bus/skill_id kwargs - self.instance = skill_creator() - - # finish initialization of skill if we didn't manage to inject skill_id and bus kwargs - # these skills only have skill_id and bus available in initialize, not in __init__ + skill_class = get_skill_class(skill_module) + self.instance = skill_class(bus=self.bus, skill_id=self.skill_id) + return self.instance is not None + except Exception as e: + LOG.warning(f"Skill load raised exception: {e}") + + try: + # attempt to use old style create_skill function entrypoint + skill_creator = get_create_skill_function(skill_module) or \ + self.skill_class + except Exception as e: + LOG.exception(f"Failed to load skill creator: {e}") + self.instance = None + return False + + # if the signature supports skill_id and bus pass them + # to fully initialize the skill in 1 go + try: + # skills that do will have bus and skill_id available + # as soon as they call super() + self.instance = skill_creator(bus=self.bus, + skill_id=self.skill_id) + except Exception as e: + # most old skills do not expose bus/skill_id kwargs + LOG.warning(f"Legacy skill: {e}") + self.instance = skill_creator() + + try: + # finish initialization of skill if we didn't manage to inject + # skill_id and bus kwargs. + # these skills only have skill_id and bus available in initialize, + # not in __init__ if not self.instance._is_fully_initialized: self.instance._startup(self.bus, self.skill_id) - except Exception as e: LOG.exception(f'Skill __init__ failed with {e}') self.instance = None @@ -439,6 +483,10 @@ def _create_skill_instance(self, skill_module=None): return self.instance is not None def _communicate_load_status(self): + """ + Check internal parameters and emit `mycroft.skills.loaded` or + `mycroft.skills.loading_failure` as appropriate + """ if self.loaded: message = Message('mycroft.skills.loaded', {"path": self.skill_directory, @@ -448,7 +496,8 @@ def _communicate_load_status(self): LOG.info(f'Skill {self.skill_id} loaded successfully') else: message = Message('mycroft.skills.loading_failure', - {"path": self.skill_directory, "id": self.skill_id}) + {"path": self.skill_directory, + "id": self.skill_id}) self.bus.emit(message) if not self.is_blacklisted: LOG.error(f'Skill {self.skill_id} failed to load') @@ -460,12 +509,24 @@ class PluginSkillLoader(SkillLoader): def __init__(self, bus, skill_id): super().__init__(bus, skill_id=skill_id) - def load(self, skill_class): + def load(self, skill_class: Optional[callable] = None) -> bool: + """ + Load a skill plugin + @param skill_class: Skill class to instantiate + @return: True if skill was loaded + """ LOG.info('ATTEMPTING TO LOAD PLUGIN SKILL: ' + self.skill_id) - self._skill_class = skill_class + self._skill_class = skill_class or self._skill_class + if not self._skill_class: + raise RuntimeError(f"_skill_class not defined for {self.skill_id}") return self._load() def _load(self): + """ + Load the skill if it is not blacklisted, emit load status, + and return load status. + @return: True if skill was loaded + """ self._prepare_for_load() if self.is_blacklisted: self._skip_load() @@ -476,10 +537,6 @@ def _load(self): self._communicate_load_status() return self.loaded - def activate(self): - self.active = True - self.load(self._skill_class) - class SkillContainer: def __init__(self, skill_id, skill_directory=None, bus=None): @@ -558,7 +615,10 @@ def _launch_standalone_skill(self): def _launch_script(): - """USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id]""" + """ + Console script entrypoint + USAGE: ovos-skill-launcher {skill_id} [path/to/my/skill_id] + """ args_count = len(sys.argv) if args_count == 2: skill_id = sys.argv[1] diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 5d5c3909..ec86d828 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -167,22 +167,32 @@ def simple_trace(stack_trace): class BaseSkill: - """Base class for mycroft skills providing common behaviour and parameters - to all Skill implementations. This base class does not require `mycroft` to be importable + """ + Base class for mycroft skills providing common behaviour and parameters + to all Skill implementations. This base class does not require `mycroft` to + be importable skill_launcher.py used to be skill_loader-py in mycroft-core - for launching skills one can use skill_launcher.py to run them standalone (eg, docker), - but the main objective is to make skills work more like proper python objects and allow usage of the class directly + for launching skills one can use skill_launcher.py to run them standalone + (eg, docker), but the main objective is to make skills work more like proper + python objects and allow usage of the class directly the considerations are: - - most skills in the wild dont expose kwargs, so dont accept skill_id or bus - - most skills expect a loader class to set up the bus and skill_id after object creation - - skills can not do pythonic things in init, instead of doing things after super() devs are expected to use initialize() which is a mycroft invention and non-standard - - main concern is that anything depending on self.skill_id being set can not be used in init method (eg. self.settings and self.file_system) - - __new__ uncouples the skill init from a helper class, making skills work like regular python objects - - the magic in `__new__` is just so we dont break everything in the wild, since we cant start requiring skill_id and bus args + - most skills in the wild don't expose kwargs, so don't accept + skill_id or bus + - most skills expect a loader class to set up the bus and skill_id after + object creation + - skills can not do pythonic things in init, instead of doing things after + super() devs are expected to use initialize() which is a mycroft invention + and non-standard + - main concern is that anything depending on self.skill_id being set can not + be used in init method (eg. self.settings and self.file_system) + - __new__ uncouples the skill init from a helper class, making skills work + like regular python objects + - the magic in `__new__` is just so we don't break everything in the wild, + since we cant start requiring skill_id and bus args KwArgs: name (str): skill name - DEPRECATED @@ -197,15 +207,21 @@ def __new__(cls, *args, **kwargs): try: # skill follows latest best practices, accepts kwargs and does its own init return super().__new__(cls, skill_id=skill_id, bus=bus) - except: + except Exception as e: + LOG.info(e) + try: # skill did not update its init method, let's do some magic to init it manually skill = super().__new__(cls, *args, **kwargs) skill._startup(bus, skill_id) return skill - - # skill loader was not used to create skill object, we are missing the kwargs - # skill wont be fully inited, please move logic to initialize - LOG.warning(f"{cls.__name__} not fully inited, self.bus and self.skill_id will only be available in self.initialize") + except Exception as e: + LOG.info(e) + + # skill loader was not used to create skill object, log a warning and + # do the legacy init + LOG.warning(f"{cls.__name__} not fully inited, self.bus and " + f"self.skill_id will only be available in self.initialize. " + f"Pass kwargs `skill_id` and `bus` to resolve this.") return super().__new__(cls) def __init__(self, name=None, bus=None, resources_dir=None, diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2842fb31..6ca78511 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos-utils < 0.1.0, >=0.0.31 +ovos-utils < 0.1.0, >=0.0.33a7 ovos_config < 0.1.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.3 diff --git a/test/unittests/test_abort.py b/test/unittests/test_decorators.py similarity index 97% rename from test/unittests/test_abort.py rename to test/unittests/test_decorators.py index 2f058943..2e7ec5b9 100644 --- a/test/unittests/test_abort.py +++ b/test/unittests/test_decorators.py @@ -204,3 +204,23 @@ def test_developer_stop_msg(self): self.bus.emitted_msgs = [] sleep(2) self.assertTrue(self.bus.emitted_msgs == []) + + +class TestConverse(unittest.TestCase): + # TODO + pass + + +class TestFallbackHandler(unittest.TestCase): + # TODO + pass + + +class TestLayers(unittest.TestCase): + # TODO + pass + + +class TestOCP(unittest.TestCase): + # TODO + pass diff --git a/test/unittests/test_filesystem.py b/test/unittests/test_filesystem.py new file mode 100644 index 00000000..dbe4d971 --- /dev/null +++ b/test/unittests/test_filesystem.py @@ -0,0 +1,40 @@ +import unittest +import shutil +from os import environ +from os.path import join, dirname, isdir +from ovos_workshop.filesystem import FileSystemAccess + + +class TestFilesystem(unittest.TestCase): + test_data_path = join(dirname(__file__), "xdg_data") + + @classmethod + def setUpClass(cls) -> None: + environ['XDG_DATA_HOME'] = cls.test_data_path + + @classmethod + def tearDownClass(cls) -> None: + data_path = environ.pop('XDG_DATA_HOME') + try: + shutil.rmtree(data_path) + except: + pass + + def test_filesystem(self): + fs = FileSystemAccess("test") + + # FS path init + self.assertEqual(fs.path, join(self.test_data_path, "mycroft", + "filesystem", "test")) + self.assertTrue(isdir(fs.path)) + + # Invalid open + with self.assertRaises(FileNotFoundError): + fs.open("test.txt", "r") + self.assertFalse(fs.exists("test.txt")) + + # Valid file creation + file = fs.open("test.txt", "w+") + self.assertIsNotNone(file) + file.close() + self.assertTrue(fs.exists("test.txt")) \ No newline at end of file diff --git a/test/unittests/test_permissions.py b/test/unittests/test_permissions.py new file mode 100644 index 00000000..c7095231 --- /dev/null +++ b/test/unittests/test_permissions.py @@ -0,0 +1,21 @@ +import unittest + +from ovos_workshop.permissions import ConverseMode, FallbackMode, ConverseActivationMode + + +class TestPermissions(unittest.TestCase): + def test_converse_mode(self): + self.assertIsInstance(ConverseMode.ACCEPT_ALL, str) + self.assertIsInstance(ConverseMode.WHITELIST, str) + self.assertIsInstance(ConverseMode.BLACKLIST, str) + + def test_fallback_mode(self): + self.assertIsInstance(FallbackMode.ACCEPT_ALL, str) + self.assertIsInstance(FallbackMode.WHITELIST, str) + self.assertIsInstance(FallbackMode.BLACKLIST, str) + + def test_converse_activation_mode(self): + self.assertIsInstance(ConverseActivationMode.ACCEPT_ALL, str) + self.assertIsInstance(ConverseActivationMode.PRIORITY, str) + self.assertIsInstance(ConverseActivationMode.WHITELIST, str) + self.assertIsInstance(ConverseActivationMode.BLACKLIST, str) diff --git a/test/unittests/test_resource_files.py b/test/unittests/test_resource_files.py new file mode 100644 index 00000000..815e88e7 --- /dev/null +++ b/test/unittests/test_resource_files.py @@ -0,0 +1,106 @@ +import unittest +import shutil + +from os import environ +from os.path import isdir, join, dirname + + +class TestResourceFileMethods(unittest.TestCase): + def test_locate_base_directories(self): + from ovos_workshop.resource_files import locate_base_directories + # TODO + + def test_locate_lang_directories(self): + from ovos_workshop.resource_files import locate_lang_directories + # TODO + + def test_resolve_resource_file(self): + from ovos_workshop.resource_files import resolve_resource_file + # TODO + + def test_find_resource(self): + from ovos_workshop.resource_files import find_resource + + +class TestResourceType(unittest.TestCase): + from ovos_workshop.resource_files import ResourceType + # TODO + + +class TestResourceFile(unittest.TestCase): + def test_resource_file(self): + from ovos_workshop.resource_files import ResourceFile + # TODO + + def test_qml_file(self): + from ovos_workshop.resource_files import QmlFile, ResourceFile + self.assertTrue(issubclass(QmlFile, ResourceFile)) + + def test_dialog_file(self): + from ovos_workshop.resource_files import DialogFile, ResourceFile + self.assertTrue(issubclass(DialogFile, ResourceFile)) + + def test_vocab_file(self): + from ovos_workshop.resource_files import VocabularyFile, ResourceFile + self.assertTrue(issubclass(VocabularyFile, ResourceFile)) + + def test_named_value_file(self): + from ovos_workshop.resource_files import NamedValueFile, ResourceFile + self.assertTrue(issubclass(NamedValueFile, ResourceFile)) + + def test_list_file(self): + from ovos_workshop.resource_files import ListFile, ResourceFile + self.assertTrue(issubclass(ListFile, ResourceFile)) + + def test_template_file(self): + from ovos_workshop.resource_files import TemplateFile, ResourceFile + self.assertTrue(issubclass(TemplateFile, ResourceFile)) + + def test_regex_file(self): + from ovos_workshop.resource_files import RegexFile, ResourceFile + self.assertTrue(issubclass(RegexFile, ResourceFile)) + + def test_word_file(self): + from ovos_workshop.resource_files import WordFile, ResourceFile + self.assertTrue(issubclass(WordFile, ResourceFile)) + + +class TestSkillResources(unittest.TestCase): + test_data_path = join(dirname(__file__), "xdg_data") + + @classmethod + def setUpClass(cls) -> None: + environ['XDG_DATA_HOME'] = cls.test_data_path + + @classmethod + def tearDownClass(cls) -> None: + data_path = environ.pop('XDG_DATA_HOME') + try: + shutil.rmtree(data_path) + except: + pass + + def test_skill_resources(self): + from ovos_workshop.resource_files import SkillResources + # TODO + + def test_core_resources(self): + from ovos_workshop.resource_files import CoreResources, SkillResources + core_res = CoreResources("en-us") + self.assertIsInstance(core_res, SkillResources) + self.assertEqual(core_res.language, "en-us") + self.assertTrue(isdir(core_res.skill_directory)) + + def test_user_resources(self): + from ovos_workshop.resource_files import UserResources, SkillResources + user_res = UserResources("en-us", "test.skill") + self.assertIsInstance(user_res, SkillResources) + self.assertEqual(user_res.language, "en-us") + self.assertEqual(user_res.skill_directory, + join(self.test_data_path, "mycroft", "resources", + "test.skill")) + + +class TestRegexExtractor(unittest.TestCase): + from ovos_workshop.resource_files import RegexExtractor + # TODO diff --git a/test/unittests/test_settings.py b/test/unittests/test_settings.py new file mode 100644 index 00000000..c917902a --- /dev/null +++ b/test/unittests/test_settings.py @@ -0,0 +1,8 @@ +import unittest +from unittest.mock import Mock + + +class TestSettings(unittest.TestCase): + from ovos_workshop.settings import SkillSettingsManager + # TODO + diff --git a/test/unittests/test_skill_classes.py b/test/unittests/test_skill_classes.py index 4dec7d90..9492d7ff 100644 --- a/test/unittests/test_skill_classes.py +++ b/test/unittests/test_skill_classes.py @@ -14,11 +14,11 @@ class OfflineSkill(OVOSSkill): @classproperty def runtime_requirements(self): return RuntimeRequirements(internet_before_load=False, - network_before_load=False, - requires_internet=False, - requires_network=False, - no_internet_fallback=True, - no_network_fallback=True) + network_before_load=False, + requires_internet=False, + requires_network=False, + no_internet_fallback=True, + no_network_fallback=True) class LANSkill(OVOSSkill): @@ -26,11 +26,11 @@ class LANSkill(OVOSSkill): def runtime_requirements(self): scans_on_init = True return RuntimeRequirements(internet_before_load=False, - network_before_load=scans_on_init, - requires_internet=False, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=False) + network_before_load=scans_on_init, + requires_internet=False, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=False) class TestSkill(OVOSSkill): @@ -77,19 +77,19 @@ def test_bus_setter(self): def test_class_property(self): self.assertEqual(OfflineSkill.runtime_requirements, RuntimeRequirements(internet_before_load=False, - network_before_load=False, - requires_internet=False, - requires_network=False, - no_internet_fallback=True, - no_network_fallback=True) + network_before_load=False, + requires_internet=False, + requires_network=False, + no_internet_fallback=True, + no_network_fallback=True) ) self.assertEqual(LANSkill.runtime_requirements, RuntimeRequirements(internet_before_load=False, - network_before_load=True, - requires_internet=False, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=False) + network_before_load=True, + requires_internet=False, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=False) ) self.assertEqual(OVOSSkill.runtime_requirements, RuntimeRequirements() diff --git a/test/unittests/test_skill_launcher.py b/test/unittests/test_skill_launcher.py new file mode 100644 index 00000000..d865e416 --- /dev/null +++ b/test/unittests/test_skill_launcher.py @@ -0,0 +1,141 @@ +import shutil +import unittest +import sys + +from os import environ +from os.path import basename, join, dirname, isdir + +from ovos_utils.messagebus import FakeBus + + +class TestSkillLauncherFunctions(unittest.TestCase): + test_data_path = join(dirname(__file__), "xdg_data") + + @classmethod + def setUpClass(cls) -> None: + environ['XDG_DATA_HOME'] = cls.test_data_path + + @classmethod + def tearDownClass(cls) -> None: + data_path = environ.pop('XDG_DATA_HOME') + try: + shutil.rmtree(data_path) + except: + pass + + def test_get_skill_directories(self): + from ovos_workshop.skill_launcher import get_skill_directories + # Default directory + mock_config = {'skills': {}} + default_directories = get_skill_directories(mock_config) + for directory in default_directories: + self.assertEqual(basename(directory), 'skills') + # Configured directory + mock_config['skills']['directory'] = 'test' + test_directories = get_skill_directories(mock_config) + for directory in test_directories: + self.assertEqual(basename(directory), 'test') + self.assertEqual(len(default_directories), len(test_directories)) + + def test_get_default_skills_directory(self): + from ovos_workshop.skill_launcher import get_default_skills_directory + # Default directory + mock_config = {'skills': {}} + default_dir = get_default_skills_directory(mock_config) + self.assertTrue(isdir(default_dir)) + self.assertEqual(basename(default_dir), 'skills') + self.assertEqual(dirname(dirname(default_dir)), self.test_data_path) + # Override directory + mock_config['skills']['directory'] = 'test' + test_dir = get_default_skills_directory(mock_config) + self.assertTrue(isdir(test_dir)) + self.assertEqual(basename(test_dir), 'test') + self.assertEqual(dirname(dirname(test_dir)), self.test_data_path) + + def test_remove_submodule_refs(self): + from ovos_workshop.skill_launcher import remove_submodule_refs + pass + + def test_load_skill_module(self): + from ovos_workshop.skill_launcher import load_skill_module + test_path = join(dirname(__file__), "skills", "test_skill", + "__init__.py") + skill_id = "test_skill.test" + module = load_skill_module(test_path, skill_id) + self.assertIn("test_skill_test", sys.modules) + self.assertIsNotNone(module) + self.assertTrue(callable(module.create_skill)) + + def test_get_skill_class(self): + from ovos_workshop.skill_launcher import get_skill_class, \ + load_skill_module + from ovos_workshop.skills.mycroft_skill import _SkillMetaclass + test_path = join(dirname(__file__), "skills", "test_skill", + "__init__.py") + skill_id = "test_skill.test" + module = load_skill_module(test_path, skill_id) + skill = get_skill_class(module) + self.assertIsNotNone(skill) + self.assertEqual(skill.__class__, _SkillMetaclass, skill.__class__) + + # Test invalid request + with self.assertRaises(ValueError): + get_skill_class(None) + + def test_get_create_skill_function(self): + from ovos_workshop.skill_launcher import get_create_skill_function, \ + load_skill_module + test_path = join(dirname(__file__), "skills", "test_skill", + "__init__.py") + skill_id = "test_skill.test" + module = load_skill_module(test_path, skill_id) + func = get_create_skill_function(module) + self.assertIsNotNone(func) + self.assertEqual(func.__name__, "create_skill") + + def test_launch_script(self): + from ovos_workshop.skill_launcher import _launch_script + # TODO + + +class TestSkillLoader(unittest.TestCase): + bus = FakeBus() + + def test_skill_loader_init(self): + from ovos_workshop.skill_launcher import SkillLoader + from ovos_utils.process_utils import RuntimeRequirements + + loader = SkillLoader(self.bus) + self.assertEqual(loader.bus, self.bus) + self.assertIsNone(loader.loaded) + self.assertIsNone(loader.skill_directory) + self.assertIsNone(loader.skill_id) + self.assertIsNone(loader.skill_class) + self.assertEqual(loader.runtime_requirements, RuntimeRequirements()) + self.assertFalse(loader.is_blacklisted) + self.assertTrue(loader.reload_allowed) + + def test_skill_loader_load_skill(self): + from ovos_workshop.skill_launcher import SkillLoader + # TODO + + +class TestPluginSkillLoader(unittest.TestCase): + bus = FakeBus() + + def test_plugin_skill_loader_init(self): + from ovos_workshop.skill_launcher import PluginSkillLoader, SkillLoader + loader = PluginSkillLoader(self.bus, "test_skill.test") + self.assertIsInstance(loader, PluginSkillLoader) + self.assertIsInstance(loader, SkillLoader) + self.assertEqual(loader.bus, self.bus) + self.assertEqual(loader.skill_id, "test_skill.test") + + def test_plugin_skill_loader_load_skill(self): + from ovos_workshop.skill_launcher import PluginSkillLoader + # TODO + + +class TestSkillContainer(unittest.TestCase): + # TODO + pass From b0bf99d44e5f93d2cb86ba7d591f38d37f8c3319 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 17 May 2023 02:49:12 +0000 Subject: [PATCH 079/154] Increment Version to 0.0.12a26 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index c75daa93..468999c2 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 25 +VERSION_ALPHA = 26 # END_VERSION_BLOCK From 7649847b245aeb52bfcc27087830eca8a4df902c Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 17 May 2023 02:49:45 +0000 Subject: [PATCH 080/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5681b9b..30b8bc1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a25](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a25) (2023-05-03) +## [0.0.12a26](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a26) (2023-05-17) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a24...0.0.12a25) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a25...0.0.12a26) + +**Implemented enhancements:** + +- Implement Unit Tests [\#89](https://github.com/OpenVoiceOS/OVOS-workshop/pull/89) ([NeonDaniel](https://github.com/NeonDaniel)) + +**Closed issues:** + +- When core restart, the skill returns an error [\#82](https://github.com/OpenVoiceOS/OVOS-workshop/issues/82) + +## [V0.0.12a25](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a25) (2023-05-03) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a24...V0.0.12a25) **Fixed bugs:** From 866ca9cce14ce61a1f17ddce40f645efd6447cb1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 17 May 2023 09:32:41 -0700 Subject: [PATCH 081/154] Refactor to remove deprecated reference (#90) --- ovos_workshop/skill_launcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 7ea8203e..89301bd3 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -18,6 +18,7 @@ from ovos_utils.file_utils import FileWatcher from ovos_utils.log import LOG from ovos_utils.process_utils import RuntimeRequirements +from ovos_utils.skills.locations import get_skill_directories as _get_skill_dirs from ovos_workshop.skills.active import ActiveSkill from ovos_workshop.skills.auto_translatable import UniversalSkill, UniversalFallback @@ -41,9 +42,8 @@ def get_skill_directories(conf=None): # TODO: Deprecate in 0.1.0 LOG.warning(f"This method has moved to `ovos_utils.skills.locations` " f"and will be removed in a future release.") - from ovos_utils.skills.locations import get_skill_directories conf = conf or Configuration() - return get_skill_directories(conf) + return _get_skill_dirs(conf) def get_default_skills_directory(conf=None): @@ -544,7 +544,7 @@ def __init__(self, skill_id, skill_directory=None, bus=None): self.bus = bus self.skill_id = skill_id if not skill_directory: # preference to local skills instead of plugins - for p in get_skill_directories(): + for p in _get_skill_dirs(): if isdir(f"{p}/{skill_id}"): skill_directory = f"{p}/{skill_id}" LOG.debug(f"found local skill {skill_id}: {skill_directory}") From 2070e020311af720e00c3a5f21ddb02d4e9201ff Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 17 May 2023 16:32:58 +0000 Subject: [PATCH 082/154] Increment Version to 0.0.12a27 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 468999c2..b960d170 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 26 +VERSION_ALPHA = 27 # END_VERSION_BLOCK From 13cab11cb79146dd1b4666a44eaa62c3be0224c4 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 17 May 2023 16:33:29 +0000 Subject: [PATCH 083/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b8bc1d..328942df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a26](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a26) (2023-05-17) +## [0.0.12a27](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a27) (2023-05-17) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a25...0.0.12a26) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a26...0.0.12a27) + +**Merged pull requests:** + +- Refactor to remove deprecated reference [\#90](https://github.com/OpenVoiceOS/OVOS-workshop/pull/90) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a26](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a26) (2023-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a25...V0.0.12a26) **Implemented enhancements:** From 22e92281159a2bcdc9710b2f7c71300427b586af Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 14 Jun 2023 23:36:41 +0100 Subject: [PATCH 084/154] fix skill initialization compat + unittests (#95) * Update base.py * Update base.py * typo * move logic to metaclass * unittest imports * unittests * better logs * simplify * automations * more legacy compat * more legacy compat tests * more legacy compat tests * keep compat * logs * bye mycroft --- .github/workflows/build_tests.yml | 4 +- .github/workflows/coverage.yml | 2 +- .github/workflows/unit_tests.yml | 12 --- ovos_workshop/skills/base.py | 24 ----- ovos_workshop/skills/mycroft_skill.py | 75 +++++++++++++- test/unittests/skills/test_mycroft_skill.py | 6 +- test/unittests/test_skill.py | 106 ++++++++++++++++++++ 7 files changed, 186 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml index 173d2d25..6764d4b7 100644 --- a/.github/workflows/build_tests.yml +++ b/.github/workflows/build_tests.yml @@ -51,4 +51,6 @@ jobs: with: # Ignore setuptools vulnerability we can't do much about ignore-vulns: | - GHSA-r9hx-vwmv-q579 \ No newline at end of file + GHSA-r9hx-vwmv-q579 + PYSEC-2023-74 + PYSEC-2022-43012 \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 39bbf2d3..5e97e0e7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,7 +33,7 @@ jobs: run: | pip install pytest pip install pytest-cov - pytest --cov=./test/unittests --cov-report=xml + pytest --cov=./ovos_workshop --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index eb8a113c..91600b7d 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -61,18 +61,6 @@ jobs: # NOTE: additional pytest invocations should also add the --cov-append flag # or they will overwrite previous invocations' coverage reports # (for an example, see OVOS Skill Manager's workflow) - - name: Replace ovos-core with mycroft-core - run: | - pip uninstall ovos-core -y - pip uninstall ovos-lingua-franca -y - pip install git+https://github.com/MycroftAI/mycroft-core - pip install . - - name: Run mycroft unittests - run: | - pytest --cov-append --cov=ovos_workshop --cov-report xml test/unittests - # NOTE: additional pytest invocations should also add the --cov-append flag - # or they will overwrite previous invocations' coverage reports - # (for an example, see OVOS Skill Manager's workflow) - name: Upload coverage env: CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index ec86d828..e3121776 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -200,30 +200,6 @@ class BaseSkill: bus (MycroftWebsocketClient): Optional bus connection """ - def __new__(cls, *args, **kwargs): - if "skill_id" in kwargs and "bus" in kwargs: - skill_id = kwargs["skill_id"] - bus = kwargs["bus"] - try: - # skill follows latest best practices, accepts kwargs and does its own init - return super().__new__(cls, skill_id=skill_id, bus=bus) - except Exception as e: - LOG.info(e) - try: - # skill did not update its init method, let's do some magic to init it manually - skill = super().__new__(cls, *args, **kwargs) - skill._startup(bus, skill_id) - return skill - except Exception as e: - LOG.info(e) - - # skill loader was not used to create skill object, log a warning and - # do the legacy init - LOG.warning(f"{cls.__name__} not fully inited, self.bus and " - f"self.skill_id will only be available in self.initialize. " - f"Pass kwargs `skill_id` and `bus` to resolve this.") - return super().__new__(cls) - def __init__(self, name=None, bus=None, resources_dir=None, settings: JsonStorage = None, gui=None, enable_settings_manager=True, diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py index b86a8c83..92d5dd23 100644 --- a/ovos_workshop/skills/mycroft_skill.py +++ b/ovos_workshop/skills/mycroft_skill.py @@ -14,16 +14,87 @@ # """Common functionality relating to the implementation of mycroft skills.""" +import inspect import shutil from abc import ABCMeta -from os.path import join, exists +from os.path import join, exists, dirname from ovos_utils.log import LOG + from ovos_workshop.skills.base import BaseSkill, is_classic_core class _SkillMetaclass(ABCMeta): - """ To override isinstance checks we need to use a metaclass """ + """ + this metaclass ensures we can load skills like regular python objects + mycroft-core required a skill loader helper class, which created the skill and then finished object init + this means skill_id and bus are not available in init method, mycroft introduced a method named initialize meant for this + + to make skills pythonic and standalone, this metaclass is used to auto init old skills and help in migrating to new standards + + To override isinstance checks we also need to use a metaclass + + TODO: remove compat ovos-core 0.2.0, including MycroftSkill class + """ + + def __call__(cls, *args, **kwargs): + from ovos_bus_client import MessageBusClient + from ovos_utils.messagebus import FakeBus + bus = None + skill_id = None + + if "bus" not in kwargs: + for a in args: + if isinstance(a, MessageBusClient) or isinstance(a, FakeBus): + bus = a + LOG.warning(f"bus should be a kwarg, guessing {a} is the bus") + break + else: + LOG.warning("skill initialized without bus!! this is legacy behaviour and" + " requires you to call skill.bind(bus) or skill._startup(skill_id, bus)\n" + "bus will be required starting on ovos-core 0.1.0") + return super().__call__(*args, **kwargs) + + if "skill_id" in kwargs: + skill_id = kwargs.pop("skill_id") + if "bus" in kwargs: + bus = kwargs.pop("bus") + if not skill_id: + LOG.warning(f"skill_id should be a kwarg, please update {cls.__name__}") + if args and isinstance(args[0], str): + a = args[0] + if a[0].isupper(): # in mycroft name is CamelCase by convention, not skill_id + LOG.debug(f"ambiguous skill_id, ignoring {a} as it appears to be a CamelCase name") + else: + LOG.warning(f"ambiguous skill_id, assuming positional argument: {a}") + skill_id = a + + if not skill_id: + LOG.warning("skill initialized without skill_id!! this is legacy behaviour and" + " requires you to call skill._startup(skill_id, bus)\n" + "skill_id will be required starting on ovos-core 0.1.0") + return super().__call__(*args, **kwargs) + + # by convention skill_id is the folder name + # usually repo.author + # TODO - uncomment once above is deprecated + #skill_id = dirname(inspect.getfile(cls)).split("/")[-1] + #LOG.warning(f"missing skill_id, assuming folder name convention: {skill_id}") + + try: + # skill follows latest best practices, accepts kwargs and does its own init + return super().__call__(skill_id=skill_id, bus=bus, **kwargs) + except TypeError: + LOG.warning("legacy skill signature detected, attempting to init skill manually, " + f"self.bus and self.skill_id will only be available in self.initialize.\n" + + f"__init__ method needs to accept `skill_id` and `bus` to resolve this.") + + # skill did not update its init method, let's do some magic to init it manually + # NOTE: no try: except because all skills must accept this initialization and we want exception + # this is what skill loader does internally + skill = super().__call__(*args, **kwargs) + skill._startup(bus, skill_id) + return skill def __instancecheck__(self, instance): if is_classic_core(): diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill.py index 071beff2..32d87fd6 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill.py @@ -20,8 +20,8 @@ from os.path import join, dirname, abspath from unittest.mock import MagicMock, patch -import pytest -from adapt.intent import IntentBuilder +#import pytest +from ovos_utils.intents import IntentBuilder from ovos_bus_client import Message from ovos_config.config import Configuration @@ -247,7 +247,7 @@ def check_register_decorators(self, result_list): sorted(result_list, key=lambda d: sorted(d.items()))) self.emitter.reset() - @pytest.mark.skip + #@pytest.mark.skip def test_register_decorators(self): """ Test decorated intents """ path_orig = sys.path diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index 801596e6..8b410561 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -12,6 +12,66 @@ from ovos_workshop.skill_launcher import SkillLoader +class LegacySkill(CoreSkill): + def __init__(self, skill_name="LegacySkill", bus=None, **kwargs): + self.inited = True + self.initialized = False + self.startup_called = False + super().__init__(skill_name, bus, **kwargs) + # __new__ calls `_startup` so this should be defined in __init__ + assert self.skill_id is not None + + def initialize(self): + self.initialized = True + + def _startup(self, bus, skill_id=""): + self.startup_called = True + self.initialize() + + +class BadLegacySkill(LegacySkill): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + print(self.bus) # not set, exception in property + + +class GoodLegacySkill(CoreSkill): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + print(self.bus) # maybe not set, exception in property + + +class SpecificArgsSkill(OVOSSkill): + def __init__(self, skill_id="SpecificArgsSkill", bus=None, **kwargs): + self.inited = True + self.initialized = False + self.startup_called = False + super().__init__(skill_id=skill_id, bus=bus, **kwargs) + self.kwargs = kwargs + + def initialize(self): + self.initialized = True + + def _startup(self, bus, skill_id=""): + self.startup_called = True + self.initialize() + + +class KwargSkill(OVOSSkill): + def __init__(self, **kwargs): + self.inited = True + self.initialized = False + self.startup_called = False + super().__init__(**kwargs) + + def initialize(self): + self.initialized = True + + def _startup(self, bus, skill_id=""): + self.startup_called = True + self.initialize() + + class TestSkill(unittest.TestCase): def setUp(self): self.bus = FakeBus() @@ -101,3 +161,49 @@ def test_stop(self): def tearDown(self) -> None: self.skill.unload() + + +class TestSkillNew(unittest.TestCase): + def test_legacy(self): + bus = FakeBus() + + # a legacy skill accepts wrong args, but accepts kwargs + legacy = LegacySkill("LegacyName", bus, skill_id="legacy.mycroft") + self.assertTrue(legacy.inited) + self.assertTrue(legacy.initialized) + self.assertTrue(legacy.startup_called) + self.assertIsNotNone(legacy.skill_id) + self.assertEqual(legacy.bus, bus) + + # a legacy skill not accepting args at all + with self.assertRaises(Exception) as ctxt: + BadLegacySkill() # accesses self.bus in __init__ + self.assertTrue("Accessed MycroftSkill.bus in __init__" in str(ctxt.exception)) + + legacynoargs = LegacySkill() # no exception this time because bus is not used in init + self.assertTrue(legacynoargs.inited) + self.assertFalse(legacynoargs.initialized) + self.assertFalse(legacynoargs.startup_called) + + # a legacy skill fully inited at once + legacy = GoodLegacySkill(skill_id="legacy.mycroft", bus=bus) # accesses self.bus in __init__ + self.assertEqual(legacy.skill_id, "legacy.mycroft") + self.assertEqual(legacy.bus, bus) + + def test_load(self): + bus = FakeBus() + kwarg = KwargSkill(skill_id="kwarg", bus=bus) + self.assertTrue(kwarg.inited) + self.assertTrue(kwarg.initialized) + self.assertTrue(kwarg.startup_called) + self.assertEqual(kwarg.skill_id, "kwarg") + self.assertEqual(kwarg.bus, bus) + + gui = Mock() + args = SpecificArgsSkill("args", bus, gui=gui) + self.assertTrue(args.inited) + self.assertTrue(args.initialized) + self.assertTrue(args.startup_called) + self.assertEqual(args.skill_id, "args") + self.assertEqual(args.bus, bus) + self.assertEqual(args.gui, gui) \ No newline at end of file From 884d9047a0ab295a9e746d203e61dc298a0db723 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Wed, 14 Jun 2023 22:37:04 +0000 Subject: [PATCH 085/154] Increment Version to 0.0.12a28 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index b960d170..049c55be 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 27 +VERSION_ALPHA = 28 # END_VERSION_BLOCK From 1426b75139e3fff260ced2909d2ebf81df98f134 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Wed, 14 Jun 2023 22:37:39 +0000 Subject: [PATCH 086/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 328942df..7e447e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a27](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a27) (2023-05-17) +## [0.0.12a28](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a28) (2023-06-14) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a26...0.0.12a27) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a27...0.0.12a28) + +**Fixed bugs:** + +- fix skill initialization compat + unittests [\#95](https://github.com/OpenVoiceOS/OVOS-workshop/pull/95) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a27](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a27) (2023-05-17) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a26...V0.0.12a27) **Merged pull requests:** From 666607c399c55f7a62421e10cd735b445494e1af Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 14 Jun 2023 18:39:50 -0700 Subject: [PATCH 087/154] Update AudioInterface reference to resolve deprecation warning (#98) --- ovos_workshop/skills/ovos.py | 4 ++-- requirements/requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index eb426821..17207de5 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -6,7 +6,7 @@ from ovos_utils.log import LOG from ovos_utils.messagebus import Message, dig_for_message from ovos_utils.skills import get_non_properties -from ovos_utils.skills.audioservice import AudioServiceInterface +from ovos_utils.skills.audioservice import OCPInterface from ovos_utils.skills.settings import PrivateSettings from ovos_utils.sound import play_audio @@ -40,7 +40,7 @@ def bind(self, bus): # here to ensure self.skill_id is populated self.private_settings = PrivateSettings(self.skill_id) self.intent_layers.bind(self) - self.audio_service = AudioServiceInterface(self.bus) + self.audio_service = OCPInterface(self.bus) # new public api, these are not available in MycroftSkill @property diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6ca78511..58ad68f9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos-utils < 0.1.0, >=0.0.33a7 +ovos-utils < 0.1.0, >=0.0.33 ovos_config < 0.1.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.3 From 6482fd0c2a1f599a1726b35dce97cb477d11d6f3 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 15 Jun 2023 01:40:12 +0000 Subject: [PATCH 088/154] Increment Version to 0.0.12a29 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 049c55be..42bc43b6 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 28 +VERSION_ALPHA = 29 # END_VERSION_BLOCK From 77460a531e1ed0deefa13e71848fc84eddccd1c5 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 15 Jun 2023 01:40:49 +0000 Subject: [PATCH 089/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e447e1b..d5536547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a28](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a28) (2023-06-14) +## [0.0.12a29](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a29) (2023-06-15) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a27...0.0.12a28) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a28...0.0.12a29) + +**Merged pull requests:** + +- Update AudioInterface reference to resolve deprecation warning [\#98](https://github.com/OpenVoiceOS/OVOS-workshop/pull/98) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a28](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a28) (2023-06-14) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a27...V0.0.12a28) **Fixed bugs:** From 80280093235f35406139242391de1c17a108ee3f Mon Sep 17 00:00:00 2001 From: builderjer <34875857+builderjer@users.noreply.github.com> Date: Fri, 16 Jun 2023 12:32:21 -0600 Subject: [PATCH 090/154] updated requirements (#99) --- requirements/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 58ad68f9..a6089e36 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,5 +2,5 @@ ovos-utils < 0.1.0, >=0.0.33 ovos_config < 0.1.0,>=0.0.4 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.3 -ovos_backend_client< 0.1.0, >= 0.0.6 -rapidfuzz \ No newline at end of file +ovos_backend_client<=0.1.0 +rapidfuzz From 7a23371b77379c985e10cf499ad2ff63c1f05e83 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 16 Jun 2023 18:32:39 +0000 Subject: [PATCH 091/154] Increment Version to 0.0.12a30 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 42bc43b6..1d3ac416 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 29 +VERSION_ALPHA = 30 # END_VERSION_BLOCK From 5e5ae7936e8397e2aba3dd6f6ff96f2bf6ae2ab5 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 16 Jun 2023 18:33:15 +0000 Subject: [PATCH 092/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5536547..49b6cd86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a29](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a29) (2023-06-15) +## [0.0.12a30](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a30) (2023-06-16) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a28...0.0.12a29) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a29...0.0.12a30) + +**Merged pull requests:** + +- updated requirements [\#99](https://github.com/OpenVoiceOS/OVOS-workshop/pull/99) ([builderjer](https://github.com/builderjer)) + +## [V0.0.12a29](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a29) (2023-06-15) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a28...V0.0.12a29) **Merged pull requests:** From 7de591a3ecee736066a8d4d1283d2350cfb8895b Mon Sep 17 00:00:00 2001 From: fidesachates <2189515+fidesachates@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:07:58 -0400 Subject: [PATCH 093/154] fix: skill reloading broken (#101) --- ovos_workshop/skill_launcher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index 89301bd3..ae9cffe5 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -384,11 +384,11 @@ def _start_filewatcher(self): callback=self._handle_filechange, recursive=True) - def _handle_filechange(self): + def _handle_filechange(self, path): """ Handle a file change notification by reloading the skill """ - LOG.info("Skill change detected!") + LOG.info(f'Skill change detected! {path}') try: if self.reload_allowed: self.reload() From 79f489404ff0362d83304ece8e15da5109040124 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 19 Jun 2023 15:08:15 +0000 Subject: [PATCH 094/154] Increment Version to 0.0.12a31 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 1d3ac416..79e91238 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 30 +VERSION_ALPHA = 31 # END_VERSION_BLOCK From 0e6646bfdcb9081a70fd43b9fc3837b3fce5ccd9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 19 Jun 2023 15:08:49 +0000 Subject: [PATCH 095/154] Update Changelog --- CHANGELOG.md | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 300 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b6cd86..eac51902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a30](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a30) (2023-06-16) +## [0.0.12a31](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a31) (2023-06-19) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a29...0.0.12a30) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a30...0.0.12a31) + +**Fixed bugs:** + +- fix: skill reloading broken [\#101](https://github.com/OpenVoiceOS/OVOS-workshop/pull/101) ([fidesachates](https://github.com/fidesachates)) + +## [V0.0.12a30](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a30) (2023-06-16) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a29...V0.0.12a30) **Merged pull requests:** @@ -253,6 +261,296 @@ - add `voc_list` helper function [\#54](https://github.com/OpenVoiceOS/OVOS-workshop/pull/54) ([emphasize](https://github.com/emphasize)) +## [V0.0.11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11) (2023-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a6...V0.0.11) + +## [V0.0.11a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a6) (2023-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a5...V0.0.11a6) + +**Merged pull requests:** + +- Update ovos\_utils dependency to stable release [\#52](https://github.com/OpenVoiceOS/OVOS-workshop/pull/52) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.11a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a5) (2023-02-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a4...V0.0.11a5) + +**Fixed bugs:** + +- allow skills to bump workshop and still work in core \< 0.0.7 [\#51](https://github.com/OpenVoiceOS/OVOS-workshop/pull/51) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.11a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a4) (2023-02-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a3...V0.0.11a4) + +**Merged pull requests:** + +- feat/generalize runtime requirements [\#49](https://github.com/OpenVoiceOS/OVOS-workshop/pull/49) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.11a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a3) (2023-02-02) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a2...V0.0.11a3) + +**Fixed bugs:** + +- replace is\_ovos with is\_classic\_core [\#48](https://github.com/OpenVoiceOS/OVOS-workshop/pull/48) ([AIIX](https://github.com/AIIX)) + +## [V0.0.11a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a2) (2023-01-30) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.11a1...V0.0.11a2) + +## [V0.0.11a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.11a1) (2023-01-28) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10...V0.0.11a1) + +**Fixed bugs:** + +- allow empty string value settings [\#47](https://github.com/OpenVoiceOS/OVOS-workshop/pull/47) ([emphasize](https://github.com/emphasize)) + +## [V0.0.10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10) (2023-01-24) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a6...V0.0.10) + +**Merged pull requests:** + +- Update ovos-core ref in unit tests [\#46](https://github.com/OpenVoiceOS/OVOS-workshop/pull/46) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.10a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a6) (2023-01-24) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a5...V0.0.10a6) + +**Merged pull requests:** + +- Update dependencies to stable versions [\#45](https://github.com/OpenVoiceOS/OVOS-workshop/pull/45) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.10a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a5) (2023-01-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a4...V0.0.10a5) + +**Fixed bugs:** + +- Fix message context handling in `__handle_stop` [\#44](https://github.com/OpenVoiceOS/OVOS-workshop/pull/44) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.10a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a4) (2023-01-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a3...V0.0.10a4) + +**Merged pull requests:** + +- Deprecate Application-managed `settings` [\#43](https://github.com/OpenVoiceOS/OVOS-workshop/pull/43) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.10a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a3) (2023-01-18) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a2...V0.0.10a3) + +**Merged pull requests:** + +- refactor the old patches module [\#34](https://github.com/OpenVoiceOS/OVOS-workshop/pull/34) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.10a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a2) (2023-01-17) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.10a1...V0.0.10a2) + +**Implemented enhancements:** + +- Get response from validated value [\#35](https://github.com/OpenVoiceOS/OVOS-workshop/pull/35) ([emphasize](https://github.com/emphasize)) + +## [V0.0.10a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.10a1) (2022-12-15) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.9...V0.0.10a1) + +**Implemented enhancements:** + +- feat/SkillNetworkRequirements [\#36](https://github.com/OpenVoiceOS/OVOS-workshop/pull/36) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.9) (2022-10-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.9a1...V0.0.9) + +## [V0.0.9a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.9a1) (2022-10-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8...V0.0.9a1) + +**Fixed bugs:** + +- Fix circular imports [\#32](https://github.com/OpenVoiceOS/OVOS-workshop/pull/32) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a6...V0.0.8) + +## [V0.0.8a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a6) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a5...V0.0.8a6) + +**Merged pull requests:** + +- shared code with ovos utils [\#31](https://github.com/OpenVoiceOS/OVOS-workshop/pull/31) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a5) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a4...V0.0.8a5) + +## [V0.0.8a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a4) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a3...V0.0.8a4) + +**Implemented enhancements:** + +- feat/resource\_utils [\#30](https://github.com/OpenVoiceOS/OVOS-workshop/pull/30) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a3) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a2...V0.0.8a3) + +**Implemented enhancements:** + +- improve send\_stop\_signal [\#29](https://github.com/OpenVoiceOS/OVOS-workshop/pull/29) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a2) (2022-10-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.8a1...V0.0.8a2) + +**Merged pull requests:** + +- move OCP to optional requirements [\#28](https://github.com/OpenVoiceOS/OVOS-workshop/pull/28) ([JarbasAl](https://github.com/JarbasAl)) +- license + vulnerability tests [\#27](https://github.com/OpenVoiceOS/OVOS-workshop/pull/27) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.8a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.8a1) (2022-09-13) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7...V0.0.8a1) + +**Merged pull requests:** + +- document results structure in ocp\_search [\#26](https://github.com/OpenVoiceOS/OVOS-workshop/pull/26) ([NeonDaniel](https://github.com/NeonDaniel)) +- feat/mycroft\_integration\_tests [\#25](https://github.com/OpenVoiceOS/OVOS-workshop/pull/25) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7) (2022-07-29) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a12...V0.0.7) + +## [V0.0.7a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a12) (2022-07-29) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a11...V0.0.7a12) + +**Merged pull requests:** + +- add initial tests for killable\_events [\#24](https://github.com/OpenVoiceOS/OVOS-workshop/pull/24) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.7a11](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a11) (2022-07-28) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a10...V0.0.7a11) + +**Implemented enhancements:** + +- feat/play\_audio [\#23](https://github.com/OpenVoiceOS/OVOS-workshop/pull/23) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.7a10](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a10) (2022-07-22) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a9...V0.0.7a10) + +**Merged pull requests:** + +- Update dependencies and config references [\#22](https://github.com/OpenVoiceOS/OVOS-workshop/pull/22) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.7a9](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a9) (2022-06-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a8...V0.0.7a9) + +## [V0.0.7a8](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a8) (2022-06-02) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a7...V0.0.7a8) + +**Fixed bugs:** + +- Fix/decouple from mycroft [\#21](https://github.com/OpenVoiceOS/OVOS-workshop/pull/21) ([NeonJarbas](https://github.com/NeonJarbas)) + +## [V0.0.7a7](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a7) (2022-05-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a6...V0.0.7a7) + +## [V0.0.7a6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a6) (2022-05-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a5...V0.0.7a6) + +## [V0.0.7a5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a5) (2022-05-09) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a4...V0.0.7a5) + +## [V0.0.7a4](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a4) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a3...V0.0.7a4) + +## [V0.0.7a3](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a3) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a2...V0.0.7a3) + +## [V0.0.7a2](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a2) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.7a1...V0.0.7a2) + +**Merged pull requests:** + +- Fix/optional adapt [\#20](https://github.com/OpenVoiceOS/OVOS-workshop/pull/20) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.7a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.7a1) (2022-05-07) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6...V0.0.7a1) + +**Fixed bugs:** + +- Fix/optional adapt [\#19](https://github.com/OpenVoiceOS/OVOS-workshop/pull/19) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.6) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6a1...V0.0.6) + +## [V0.0.6a1](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.6a1) (2022-03-03) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5...V0.0.6a1) + +**Breaking changes:** + +- remove/skillgui\_patches [\#18](https://github.com/OpenVoiceOS/OVOS-workshop/pull/18) ([JarbasAl](https://github.com/JarbasAl)) + +**Closed issues:** + +- OVOSSkill class inherited skills do not initialize [\#17](https://github.com/OpenVoiceOS/OVOS-workshop/issues/17) + +## [V0.0.5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5) (2022-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5a12...V0.0.5) + +## [V0.0.5a12](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5a12) (2022-02-25) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/d9261b124f73a3e4d50c6edfcd9c2243b2bc3cf6...V0.0.5a12) + +**Implemented enhancements:** + +- add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) +- Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) +- add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) +- Add new work in progress audio player ui for media service [\#6](https://github.com/OpenVoiceOS/OVOS-workshop/pull/6) ([AIIX](https://github.com/AIIX)) +- Add next and previous buttons [\#4](https://github.com/OpenVoiceOS/OVOS-workshop/pull/4) ([AIIX](https://github.com/AIIX)) +- add player pos property and fix mycroft players for plugin [\#3](https://github.com/OpenVoiceOS/OVOS-workshop/pull/3) ([AIIX](https://github.com/AIIX)) + +**Fixed bugs:** + +- Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) +- remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) +- fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) +- fix icon paths and lower version [\#7](https://github.com/OpenVoiceOS/OVOS-workshop/pull/7) ([AIIX](https://github.com/AIIX)) +- fix AudioPlayer property name [\#5](https://github.com/OpenVoiceOS/OVOS-workshop/pull/5) ([AIIX](https://github.com/AIIX)) +- fix condition in video player [\#2](https://github.com/OpenVoiceOS/OVOS-workshop/pull/2) ([AIIX](https://github.com/AIIX)) +- add a timeout to videoplayer when nothing is playing for more than 60 seconds [\#1](https://github.com/OpenVoiceOS/OVOS-workshop/pull/1) ([AIIX](https://github.com/AIIX)) + +**Merged pull requests:** + +- Feat/workflows [\#16](https://github.com/OpenVoiceOS/OVOS-workshop/pull/16) ([JarbasAl](https://github.com/JarbasAl)) +- feat/pypi\_automation [\#13](https://github.com/OpenVoiceOS/OVOS-workshop/pull/13) ([JarbasAl](https://github.com/JarbasAl)) + \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 171bbfb38a9f2d5a5cd1f5c06dc8ccfa35ac55d4 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:48:24 +0100 Subject: [PATCH 096/154] feat/session_id_wait_while_speaking (#102) --- ovos_workshop/skills/base.py | 26 ++++++++++++++++++++------ requirements/requirements.txt | 6 +++--- test/unittests/test_decorators.py | 13 ++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index e3121776..78541029 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -25,6 +25,7 @@ from threading import Event from typing import List +from ovos_bus_client.session import SessionManager from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list from lingua_franca.parse import yes_or_no, extract_number @@ -47,7 +48,7 @@ from ovos_utils.parse import match_one from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties -from ovos_utils.sound import play_acknowledge_sound, wait_while_speaking +from ovos_utils.sound import play_acknowledge_sound from ovos_workshop.decorators import classproperty from ovos_workshop.decorators.killable import AbortEvent @@ -838,11 +839,14 @@ def _activate(self): msg = dig_for_message() or Message("") if "skill_id" not in msg.context: msg.context["skill_id"] = self.skill_id - self.bus.emit(msg.forward("intent.service.skills.activate", - data={"skill_id": self.skill_id})) + + m1 = msg.forward("intent.service.skills.activate", data={"skill_id": self.skill_id}) + self.bus.emit(m1) + # backwards compat with mycroft-core - self.bus.emit(msg.forward("active_skill_request", - data={"skill_id": self.skill_id})) + # TODO - remove soon + m2 = msg.forward("active_skill_request", data={"skill_id": self.skill_id}) + self.bus.emit(m2) # method not present in mycroft-core def _deactivate(self): @@ -1701,7 +1705,17 @@ def speak(self, utterance, expect_response=False, wait=False, meta=None): self.bus.emit(m) if wait: - wait_while_speaking() + sessid = SessionManager.get(m).session_id + event = Event() + + def handle_output_end(msg): + sess = SessionManager.get(msg) + if sessid == sess.session_id: + event.set() + + self.bus.on("recognizer_loop:audio_output_end", handle_output_end) + event.wait(timeout=15) + self.bus.remove("recognizer_loop:audio_output_end", handle_output_end) def speak_dialog(self, key, data=None, expect_response=False, wait=False): """ Speak a random sentence from a dialog file. diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a6089e36..9ba0ae14 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ -ovos-utils < 0.1.0, >=0.0.33 -ovos_config < 0.1.0,>=0.0.4 +ovos-utils < 0.1.0, >=0.0.34 +ovos_config < 0.1.0,>=0.0.5 ovos-lingua-franca~=0.4,>=0.4.6 -ovos-bus-client < 0.1.0, >=0.0.3 +ovos-bus-client < 0.1.0, >=0.0.5a1 ovos_backend_client<=0.1.0 rapidfuzz diff --git a/test/unittests/test_decorators.py b/test/unittests/test_decorators.py index 2e7ec5b9..bf22caa4 100644 --- a/test/unittests/test_decorators.py +++ b/test/unittests/test_decorators.py @@ -117,7 +117,8 @@ def test_get_response(self): confirm only get_response is aborted, speech after is still spoken""" self.bus.emitted_msgs = [] # skill will enter a infinite loop unless aborted - self.bus.emit(Message(f"{self.skill.skill_id}:test2.intent")) + self.bus.emit(Message(f"{self.skill.skill_id}:test2.intent", + context={"session": {"session_id": "123"}})) sleep(2) # check that intent triggered start_msg = {'type': 'mycroft.skill.handler.start', @@ -127,10 +128,12 @@ def test_get_response(self): 'expect_response': True, 'meta': {'dialog': 'question', 'data': {}, 'skill': 'abort.test'}, 'lang': 'en-us'}} - if not is_classic_core(): - activate_msg = {'type': 'intent.service.skills.activate', 'data': {'skill_id': 'abort.test'}} - else: - activate_msg = {'type': 'active_skill_request', 'data': {'skill_id': 'abort.test'}} + activate_msg = {'type': 'intent.service.skills.activate', 'data': {'skill_id': 'abort.test'}} + + sleep(0.5) # fake wait_while_speaking + self.bus.emit(Message(f"recognizer_loop:audio_output_end", + context={"session": {"session_id": "123"}})) + sleep(1) # get_response is in a thread so it can be killed, let it capture msg above self.assertIn(start_msg, self.bus.emitted_msgs) self.assertIn(speak_msg, self.bus.emitted_msgs) From 0117379249a3e6160cd04184894133d08b4cb9a0 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 24 Jun 2023 16:48:43 +0000 Subject: [PATCH 097/154] Increment Version to 0.0.12a32 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 79e91238..0a2a4088 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 31 +VERSION_ALPHA = 32 # END_VERSION_BLOCK From 5617db15639166f0c7cb80e469065452ee7129d3 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 24 Jun 2023 16:49:19 +0000 Subject: [PATCH 098/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eac51902..da18ceca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a31](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a31) (2023-06-19) +## [0.0.12a32](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a32) (2023-06-24) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a30...0.0.12a31) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a31...0.0.12a32) + +**Implemented enhancements:** + +- feat/session\_id\_wait\_while\_speaking [\#102](https://github.com/OpenVoiceOS/OVOS-workshop/pull/102) ([JarbasAl](https://github.com/JarbasAl)) + +**Closed issues:** + +- handle file change errors in logs [\#100](https://github.com/OpenVoiceOS/OVOS-workshop/issues/100) + +## [V0.0.12a31](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a31) (2023-06-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a30...V0.0.12a31) **Fixed bugs:** From 85927ec8f9ca0edbf0159f31d05109849fcc64d7 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Tue, 4 Jul 2023 15:49:09 +0100 Subject: [PATCH 099/154] fix/play_audio (#105) --- ovos_workshop/skills/ovos.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 17207de5..8cee649e 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -71,19 +71,23 @@ def deactivate(self): self._deactivate() def play_audio(self, filename): + core_supported = False if not is_classic_core(): try: from mycroft.version import OVOS_VERSION_BUILD, OVOS_VERSION_MINOR, OVOS_VERSION_MAJOR if OVOS_VERSION_MAJOR >= 1 or \ OVOS_VERSION_MINOR > 0 or \ OVOS_VERSION_BUILD >= 4: - self.bus.emit(Message("mycroft.audio.queue", - {"filename": filename})) - return - except: - pass - LOG.warning("self.play_audio requires ovos-core >= 0.0.4a45, falling back to local skill playback") - play_audio(filename).wait() + core_supported = True # min version of ovos-core + except ImportError: # skills don't require core anymore, running standalone + core_supported = True + + if core_supported: + message = dig_for_message() or Message("") + self.bus.emit(message.forward("mycroft.audio.queue", {"filename": filename})) + else: + LOG.warning("self.play_audio requires ovos-core >= 0.0.4a45, falling back to local skill playback") + play_audio(filename).wait() @property def core_lang(self): From a52e632cda7a71e874e964b430ff24395befe9f2 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 4 Jul 2023 14:49:26 +0000 Subject: [PATCH 100/154] Increment Version to 0.0.12a33 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 0a2a4088..c3f573e2 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 32 +VERSION_ALPHA = 33 # END_VERSION_BLOCK From fc66fc46ffd70807c8c8e9435ddcd59361e7c451 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Tue, 4 Jul 2023 14:49:56 +0000 Subject: [PATCH 101/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da18ceca..89e4922b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a32](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a32) (2023-06-24) +## [0.0.12a33](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a33) (2023-07-04) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a31...0.0.12a32) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a32...0.0.12a33) + +**Fixed bugs:** + +- fix/play\_audio [\#105](https://github.com/OpenVoiceOS/OVOS-workshop/pull/105) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a32](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a32) (2023-06-24) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a31...V0.0.12a32) **Implemented enhancements:** From 0cdf027a7d32c2108a870af01f4a6cdf1a508bab Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 6 Jul 2023 11:10:54 -0700 Subject: [PATCH 102/154] Refactor `SkillGUI` with unit tests (#106) --- ovos_workshop/skills/base.py | 95 +++++++-------------------- requirements/requirements.txt | 2 +- test/unittests/skills/gui/ui/test.qml | 0 test/unittests/test_skill.py | 47 ++++++++++++- 4 files changed, 67 insertions(+), 77 deletions(-) create mode 100644 test/unittests/skills/gui/ui/test.qml diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 78541029..bd769880 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -21,7 +21,7 @@ from hashlib import md5 from inspect import signature from itertools import chain -from os.path import join, abspath, dirname, basename, isfile +from os.path import join, abspath, dirname, basename, isfile, isdir from threading import Event from typing import List @@ -38,12 +38,12 @@ from ovos_utils.enclosure.api import EnclosureAPI from ovos_utils.events import EventSchedulerInterface from ovos_utils.file_utils import FileWatcher -from ovos_utils.gui import GUIInterface +from ovos_utils.gui import GUIInterface, get_ui_directories from ovos_utils.intents import ConverseTracker from ovos_utils.intents import Intent, IntentBuilder from ovos_utils.intents.intent_service_interface import munge_regex, munge_intent_parser, IntentServiceInterface from ovos_utils.json_helper import merge_dict -from ovos_utils.log import LOG +from ovos_utils.log import LOG, deprecated from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang from ovos_utils.parse import match_one from ovos_utils.process_utils import RuntimeRequirements @@ -81,76 +81,6 @@ def is_classic_core(): return False # standalone -class SkillGUI(GUIInterface): - """SkillGUI - Interface to the Graphical User Interface - - Values set in this class are synced to the GUI, accessible within QML - via the built-in sessionData mechanism. For example, in Python you can - write in a skill: - self.gui['temp'] = 33 - self.gui.show_page('Weather.qml') - Then in the Weather.qml you'd access the temp via code such as: - text: sessionData.time - """ - - def __init__(self, skill): - self.skill = skill - super().__init__(skill.skill_id, config=Configuration()) - - @property - def bus(self): - if self.skill: - return self.skill.bus - - @property - def skill_id(self): - return self.skill.skill_id - - def setup_default_handlers(self): - """Sets the handlers for the default messages.""" - msg_type = self.build_message_type('set') - self.skill.add_event(msg_type, self.gui_set) - - def register_handler(self, event, handler): - """Register a handler for GUI events. - - When using the triggerEvent method from Qt - triggerEvent("event", {"data": "cool"}) - - Args: - event (str): event to catch - handler: function to handle the event - """ - msg_type = self.build_message_type(event) - self.skill.add_event(msg_type, handler) - - def _pages2uri(self, page_names): - # Convert pages to full reference - page_urls = [] - for name in page_names: - page = self.skill._resources.locate_qml_file(name) - if page: - if self.remote_url: - page_urls.append(self.remote_url + "/" + page) - elif page.startswith("file://"): - page_urls.append(page) - else: - page_urls.append("file://" + page) - else: - raise FileNotFoundError(f"Unable to find page: {name}") - - return page_urls - - def shutdown(self): - """Shutdown gui interface. - - Clear pages loaded through this interface and remove the skill - reference to make ref counting warning more precise. - """ - self.release() - self.skill = None - - def simple_trace(stack_trace): """Generate a simplified traceback. @@ -1954,3 +1884,22 @@ def get_scheduled_event_status(self, name): def cancel_all_repeating_events(self): """Cancel any repeating events started by the skill.""" return self.event_scheduler.cancel_all_repeating_events() + + +class SkillGUI(GUIInterface): + def __init__(self, skill: BaseSkill): + """ + Wraps `GUIInterface` for use with a skill. + """ + self._skill = skill + skill_id = skill.skill_id + bus = skill.bus + config = skill.config_core.get('gui') + ui_directories = get_ui_directories(skill.root_dir) + GUIInterface.__init__(self, skill_id=skill_id, bus=bus, config=config, + ui_directories=ui_directories) + + @property + @deprecated("`skill` should not be referenced directly", "0.1.0") + def skill(self): + return self._skill diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9ba0ae14..5d45b42c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -ovos-utils < 0.1.0, >=0.0.34 +ovos-utils < 0.1.0, >=0.0.35a6 ovos_config < 0.1.0,>=0.0.5 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.5a1 diff --git a/test/unittests/skills/gui/ui/test.qml b/test/unittests/skills/gui/ui/test.qml new file mode 100644 index 00000000..e69de29b diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index 8b410561..7e15151a 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -1,6 +1,6 @@ import json import unittest -from unittest.mock import Mock +from unittest.mock import Mock, patch from ovos_bus_client import Message @@ -8,7 +8,7 @@ from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core from mycroft.skills import MycroftSkill as CoreSkill from ovos_utils.messagebus import FakeBus -from os.path import dirname +from os.path import dirname, join from ovos_workshop.skill_launcher import SkillLoader @@ -206,4 +206,45 @@ def test_load(self): self.assertTrue(args.startup_called) self.assertEqual(args.skill_id, "args") self.assertEqual(args.bus, bus) - self.assertEqual(args.gui, gui) \ No newline at end of file + self.assertEqual(args.gui, gui) + + +class TestSkillGui(unittest.TestCase): + class LegacySkill(Mock): + skill_id = "old_skill" + bus = FakeBus() + config_core = {"gui": {"test": True, + "legacy": True}} + root_dir = join(dirname(__file__), "skills", "gui") + + class GuiSkill(Mock): + skill_id = "new_skill" + bus = FakeBus() + config_core = {"gui": {"test": True, + "legacy": False}} + root_dir = join(dirname(__file__), "skills") + + @patch("ovos_workshop.skills.base.GUIInterface.__init__") + def test_skill_gui(self, interface_init): + from ovos_utils.gui import GUIInterface + from ovos_workshop.skills.base import SkillGUI + + # Old skill with `ui` directory in root + old_skill = self.LegacySkill() + old_gui = SkillGUI(old_skill) + self.assertEqual(old_gui.skill, old_skill) + self.assertIsInstance(old_gui, GUIInterface) + interface_init.assert_called_once_with( + old_gui, skill_id=old_skill.skill_id, bus=old_skill.bus, + config=old_skill.config_core['gui'], + ui_directories={"qt5": join(old_skill.root_dir, "ui")}) + + # New skill with `gui` directory in root + new_skill = self.GuiSkill() + new_gui = SkillGUI(new_skill) + self.assertEqual(new_gui.skill, new_skill) + self.assertIsInstance(new_gui, GUIInterface) + interface_init.assert_called_with( + new_gui, skill_id=new_skill.skill_id, bus=new_skill.bus, + config=new_skill.config_core['gui'], + ui_directories={"all": join(new_skill.root_dir, "gui")}) From 309c8cd8c2b2766497f44f98b4d20c5af1540a08 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 6 Jul 2023 18:11:14 +0000 Subject: [PATCH 103/154] Increment Version to 0.0.12a34 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index c3f573e2..c87eb66b 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 33 +VERSION_ALPHA = 34 # END_VERSION_BLOCK From 0a6c48310c9e34c12baa0f22a79f878982fb8ae9 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 6 Jul 2023 18:11:49 +0000 Subject: [PATCH 104/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e4922b..2924d046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a33](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a33) (2023-07-04) +## [0.0.12a34](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a34) (2023-07-06) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a32...0.0.12a33) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a33...0.0.12a34) + +**Merged pull requests:** + +- Refactor `SkillGUI` with unit tests [\#106](https://github.com/OpenVoiceOS/OVOS-workshop/pull/106) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a33](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a33) (2023-07-04) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a32...V0.0.12a33) **Fixed bugs:** From 671c8699498dada8cf64626817f1981d4f3cfbf1 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 6 Jul 2023 20:34:26 -0700 Subject: [PATCH 105/154] Decorator module tests, docstrings, and annotations (#107) --- ovos_workshop/decorators/__init__.py | 92 +++++---- ovos_workshop/decorators/converse.py | 18 +- ovos_workshop/decorators/fallback_handler.py | 4 + ovos_workshop/decorators/killable.py | 37 +++- ovos_workshop/decorators/layers.py | 93 ++++++--- ovos_workshop/decorators/ocp.py | 29 ++- ovos_workshop/skills/base.py | 2 +- test/unittests/test_decorators.py | 205 +++++++++++++++++-- 8 files changed, 374 insertions(+), 106 deletions(-) diff --git a/ovos_workshop/decorators/__init__.py b/ovos_workshop/decorators/__init__.py index e135dd54..bc00d219 100644 --- a/ovos_workshop/decorators/__init__.py +++ b/ovos_workshop/decorators/__init__.py @@ -1,24 +1,18 @@ +from ovos_utils.log import log_deprecation from ovos_workshop.decorators.killable import killable_intent, killable_event from ovos_workshop.decorators.layers import enables_layer, \ disables_layer, layer_intent, removes_layer, resets_layers, replaces_layer -from ovos_workshop.decorators.converse import converse_handler -from ovos_workshop.decorators.fallback_handler import fallback_handler -from ovos_utils import classproperty +from ovos_workshop.decorators.ocp import ocp_play, ocp_pause, ocp_resume, \ + ocp_search, ocp_previous, ocp_featured_media from functools import wraps -try: - from ovos_workshop.decorators.ocp import ocp_next, ocp_play, ocp_pause, ocp_resume, ocp_search, ocp_previous, ocp_featured_media -except ImportError: - pass # these imports are only available if extra requirements are installed - -""" -Decorators for use with MycroftSkill methods -Helper decorators for handling context from skills. -""" +# TODO: Deprecate unused import retained for backwards-compat. +from ovos_utils import classproperty -def adds_context(context, words=''): - """Decorator adding context to the Adapt context manager. +def adds_context(context: str, words: str = ''): + """ + Decorator to add context to the Adapt context manager. Args: context (str): context Keyword to insert @@ -31,14 +25,13 @@ def func_wrapper(*args, **kwargs): ret = func(*args, **kwargs) args[0].set_context(context, words) return ret - return func_wrapper - return context_add_decorator -def removes_context(context): - """Decorator removing context from the Adapt context manager. +def removes_context(context: str): + """ + Decorator to remove context from the Adapt context manager. Args: context (str): Context keyword to remove @@ -50,14 +43,15 @@ def func_wrapper(*args, **kwargs): ret = func(*args, **kwargs) args[0].remove_context(context) return ret - return func_wrapper - return context_removes_decorator -def intent_handler(intent_parser): - """Decorator for adding a method as an intent handler.""" +def intent_handler(intent_parser: object): + """ + Decorator for adding a method as an intent handler. + @param intent_parser: string intent name or adapt.IntentBuilder object + """ def real_decorator(func): # Store the intent_parser inside the function @@ -70,12 +64,10 @@ def real_decorator(func): return real_decorator -def intent_file_handler(intent_file): - """Decorator for adding a method as an intent file handler. - - This decorator is deprecated, use intent_handler for the same effect. +def intent_file_handler(intent_file: str): + """ + Deprecated decorator for adding a method as an intent file handler. """ - def real_decorator(func): # Store the intent_file inside the function # This will be used later to call register_intent_file @@ -83,14 +75,15 @@ def real_decorator(func): func.intent_files = [] func.intent_files.append(intent_file) return func - + log_deprecation(f"Use `@intent_handler({intent_file})`", "0.1.0") return real_decorator -def resting_screen_handler(name): - """Decorator for adding a method as an resting screen handler. - - If selected will be shown on screen when device enters idle mode. +def resting_screen_handler(name: str): + """ + Decorator for adding a method as a resting screen handler to optionally + be shown on screen when device enters idle mode. + @param name: Name of the restring screen to register """ def real_decorator(func): @@ -103,13 +96,38 @@ def real_decorator(func): return real_decorator -def skill_api_method(func): - """Decorator for adding a method to the skill's public api. - - Methods with this decorator will be registered on the message bus - and an api object can be created for interaction with the skill. +def skill_api_method(func: callable): + """ + Decorator for adding a method to the skill's public api. Methods with this + decorator will be registered on the messagebus and an api object can be + created for interaction with the skill. + @param func: API method to expose """ # tag the method by adding an api_method member to it if not hasattr(func, 'api_method') and hasattr(func, '__name__'): func.api_method = True return func + + +def converse_handler(func): + """ + Decorator for aliasing a method as the converse method + """ + if not hasattr(func, 'converse'): + func.converse = True + return func + + +def fallback_handler(priority: int = 50): + """ + Decorator for adding a fallback intent handler. + + @param priority: Fallback priority (0-100) with lower values having higher + priority + """ + def real_decorator(func): + if not hasattr(func, 'fallback_priority'): + func.fallback_priority = priority + return func + + return real_decorator diff --git a/ovos_workshop/decorators/converse.py b/ovos_workshop/decorators/converse.py index 126761d7..f4e5fcc2 100644 --- a/ovos_workshop/decorators/converse.py +++ b/ovos_workshop/decorators/converse.py @@ -1,11 +1,11 @@ +from ovos_utils.log import log_deprecation -def converse_handler(): - """Decorator for aliasing a method as the converse method""" - - def real_decorator(func): - if not hasattr(func, 'converse'): - func.converse = True - return func - - return real_decorator +def converse_handler(func): + """ + Decorator for aliasing a method as the converse method + """ + log_deprecation("Import from `ovos_workshop.decorators`", "0.1.0") + if not hasattr(func, 'converse'): + func.converse = True + return func diff --git a/ovos_workshop/decorators/fallback_handler.py b/ovos_workshop/decorators/fallback_handler.py index 56b79c49..76ff7861 100644 --- a/ovos_workshop/decorators/fallback_handler.py +++ b/ovos_workshop/decorators/fallback_handler.py @@ -1,5 +1,9 @@ +from ovos_utils.log import log_deprecation + def fallback_handler(priority=50): + log_deprecation("Import from `ovos_workshop.decorators`", "0.1.0") + def real_decorator(func): if not hasattr(func, 'fallback_priority'): func.fallback_priority = priority diff --git a/ovos_workshop/decorators/killable.py b/ovos_workshop/decorators/killable.py index 65e2f651..cb65aa5c 100644 --- a/ovos_workshop/decorators/killable.py +++ b/ovos_workshop/decorators/killable.py @@ -1,3 +1,5 @@ +from typing import Callable, Optional, Type + import time from ovos_utils import create_killable_daemon from ovos_utils.messagebus import Message @@ -18,16 +20,36 @@ class AbortQuestion(AbortEvent): """ gracefully abort get_response queries """ -def killable_intent(msg="mycroft.skills.abort_execution", - callback=None, react_to_stop=True, call_stop=True, - stop_tts=True): +def killable_intent(msg: str = "mycroft.skills.abort_execution", + callback: Optional[callable] = None, + react_to_stop: bool = True, + call_stop: bool = True, stop_tts: bool = True) -> callable: + """ + Decorator to mark an intent that can be terminated during execution. + @param msg: Message name to terminate on + @param callback: Optional function or method to call on termination + @param react_to_stop: If true, also terminate on `stop` Messages + @param call_stop: If true, also call `Class.stop` method + @param stop_tts: If true, emit message to stop TTS audio playback + """ return killable_event(msg, AbortIntent, callback, react_to_stop, call_stop, stop_tts) -def killable_event(msg="mycroft.skills.abort_execution", exc=AbortEvent, - callback=None, react_to_stop=False, call_stop=False, - stop_tts=False): +def killable_event(msg: str = "mycroft.skills.abort_execution", + exc: Type[Exception] = AbortEvent, + callback: Optional[callable] = None, + react_to_stop: bool = False, call_stop: bool = False, + stop_tts: bool = False): + """ + Decorator to mark a method that can be terminated during execution. + @param msg: Message name to terminate on + @param exc: Exception to raise in killed thread + @param callback: Optional function or method to call on termination + @param react_to_stop: If true, also terminate on `stop` Messages + @param call_stop: If true, also call `Class.stop` method + @param stop_tts: If true, emit message to stop TTS audio playback + """ # Begin wrapper def create_killable(func): @@ -54,7 +76,7 @@ def abort(_): try: while t.is_alive(): t.raise_exc(exc) - time.sleep(0.1) + t.join(1) except threading.ThreadError: pass # already killed except AssertionError: @@ -79,4 +101,3 @@ def abort(_): return call_function return create_killable - diff --git a/ovos_workshop/decorators/layers.py b/ovos_workshop/decorators/layers.py index e39ca520..569e6adc 100644 --- a/ovos_workshop/decorators/layers.py +++ b/ovos_workshop/decorators/layers.py @@ -1,35 +1,44 @@ import inspect from functools import wraps +from typing import Optional, List + +from ovos_bus_client import MessageBusClient from ovos_utils.log import LOG +from ovos_workshop.skills.base import BaseSkill -def dig_for_skill(max_records: int = 10): - from ovos_workshop.app import OVOSAbstractApplication - from ovos_workshop.skills import MycroftSkill +def dig_for_skill(max_records: int = 10) -> Optional[object]: + """ + Dig through the call stack to locate a Skill object + @param max_records: maximum number of records in the stack to check + @return: Skill or AbstractApplication instance if found + """ stack = inspect.stack()[1:] # First frame will be this function call stack = stack if len(stack) <= max_records else stack[:max_records] for record in stack: args = inspect.getargvalues(record.frame) if args.locals.get("self"): obj = args.locals["self"] - if isinstance(obj, MycroftSkill) or \ - isinstance(obj, OVOSAbstractApplication): + if isinstance(obj, BaseSkill): return obj elif args.locals.get("args"): for obj in args.locals["args"]: - if isinstance(obj, MycroftSkill) or \ - isinstance(obj, OVOSAbstractApplication): + if isinstance(obj, BaseSkill): return obj return None -def enables_layer(layer_name): +def enables_layer(layer_name: str): + """ + Decorator to enable an intent layer when a method is called + @param layer_name: name of intent layer to enable + """ def layer_handler(func): @wraps(func) def call_function(*args, **kwargs): skill = dig_for_skill() skill.intent_layers = skill.intent_layers or \ - IntentLayers().bind(skill) + IntentLayers().bind(skill) func(*args, **kwargs) skill.intent_layers.activate_layer(layer_name) @@ -38,13 +47,17 @@ def call_function(*args, **kwargs): return layer_handler -def disables_layer(layer_name): +def disables_layer(layer_name: str): + """ + Decorator to disable an intent layer when a method is called + @param layer_name: name of intent layer to disable + """ def layer_handler(func): @wraps(func) def call_function(*args, **kwargs): skill = dig_for_skill() skill.intent_layers = skill.intent_layers or \ - IntentLayers().bind(skill) + IntentLayers().bind(skill) func(*args, **kwargs) skill.intent_layers.deactivate_layer(layer_name) @@ -53,13 +66,18 @@ def call_function(*args, **kwargs): return layer_handler -def replaces_layer(layer_name, intent_list): +def replaces_layer(layer_name: str, intent_list: Optional[List[str]]): + """ + Replaces intents at the specified layer + @param layer_name: name of intent layer to replace + @param intent_list: list of new intents for the specified layer + """ def layer_handler(func): @wraps(func) def call_function(*args, **kwargs): skill = dig_for_skill() skill.intent_layers = skill.intent_layers or \ - IntentLayers().bind(skill) + IntentLayers().bind(skill) func(*args, **kwargs) skill.intent_layers.replace_layer(layer_name, intent_list) @@ -68,15 +86,19 @@ def call_function(*args, **kwargs): return layer_handler -def removes_layer(layer_name, intent_list): +def removes_layer(layer_name: str): + """ + Decorator to remove an intent layer when a method is called + @param layer_name: name of intent layer to remove + """ def layer_handler(func): @wraps(func) def call_function(*args, **kwargs): skill = dig_for_skill() skill.intent_layers = skill.intent_layers or \ - IntentLayers().bind(skill) + IntentLayers().bind(skill) func(*args, **kwargs) - skill.intent_layers.replace_layer(layer_name, intent_list) + skill.intent_layers.remove_layer(layer_name) return call_function @@ -84,12 +106,15 @@ def call_function(*args, **kwargs): def resets_layers(): + """ + Decorator to reset and disable intent layers + """ def layer_handler(func): @wraps(func) def call_function(*args, **kwargs): skill = dig_for_skill() skill.intent_layers = skill.intent_layers or \ - IntentLayers().bind(skill) + IntentLayers().bind(skill) func(*args, **kwargs) skill.intent_layers.disable() @@ -98,9 +123,13 @@ def call_function(*args, **kwargs): return layer_handler -def layer_intent(intent_parser, layer_name): - """Decorator for adding a method as an intent handler belonging to an - intent layer.""" +def layer_intent(intent_parser: callable, layer_name: str): + """ + Decorator for adding a method as an intent handler belonging to an + intent layer. + @param intent_parser: intent parser method + @param layer_name: name of intent layer intent is associated with + """ def real_decorator(func): # Store the intent_parser inside the function @@ -135,25 +164,25 @@ def __init__(self): self._layers = {} self._active_layers = [] - def bind(self, skill): + def bind(self, skill: object): if skill: self._skill = skill return self @property - def skill(self): + def skill(self) -> BaseSkill: return self._skill @property - def bus(self): + def bus(self) -> Optional[MessageBusClient]: return self._skill.bus if self._skill else None @property - def skill_id(self): + def skill_id(self) -> str: return self._skill.skill_id if self._skill else "IntentLayers" @property - def active_layers(self): + def active_layers(self) -> List[str]: return self._active_layers def disable(self): @@ -162,7 +191,8 @@ def disable(self): for layer_name, intents in self._layers.items(): self.deactivate_layer(layer_name) - def update_layer(self, layer_name, intent_list=None): + def update_layer(self, layer_name: str, + intent_list: Optional[List[str]] = None): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" intent_list = intent_list or [] @@ -171,7 +201,7 @@ def update_layer(self, layer_name, intent_list=None): self._layers[layer_name] += intent_list or [] LOG.info(f"Adding {intent_list} to {layer_name}") - def activate_layer(self, layer_name): + def activate_layer(self, layer_name: str): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" if layer_name in self._layers: @@ -183,7 +213,7 @@ def activate_layer(self, layer_name): else: LOG.debug("no layer named: " + layer_name) - def deactivate_layer(self, layer_name): + def deactivate_layer(self, layer_name: str): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" if layer_name in self._layers: @@ -195,7 +225,7 @@ def deactivate_layer(self, layer_name): else: LOG.debug("no layer named: " + layer_name) - def remove_layer(self, layer_name): + def remove_layer(self, layer_name: str): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" if layer_name in self._layers: @@ -205,7 +235,8 @@ def remove_layer(self, layer_name): else: LOG.debug("no layer named: " + layer_name) - def replace_layer(self, layer_name, intent_list=None): + def replace_layer(self, layer_name: str, + intent_list: Optional[List[str]] = None): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" if layer_name in self._layers: @@ -214,7 +245,7 @@ def replace_layer(self, layer_name, intent_list=None): else: self.update_layer(layer_name, intent_list) - def is_active(self, layer_name): + def is_active(self, layer_name: str): if not layer_name.startswith(f"{self.skill_id}:"): layer_name = f"{self.skill_id}:{layer_name}" return layer_name in self.active_layers diff --git a/ovos_workshop/decorators/ocp.py b/ovos_workshop/decorators/ocp.py index 532fad71..164563a0 100644 --- a/ovos_workshop/decorators/ocp.py +++ b/ovos_workshop/decorators/ocp.py @@ -30,7 +30,9 @@ def real_decorator(func): def ocp_play(): - """Decorator for adding a method as an common play search handler.""" + """ + Decorator for adding a method to handle media playback. + """ def real_decorator(func): # Store the flag inside the function @@ -44,7 +46,9 @@ def real_decorator(func): def ocp_previous(): - """Decorator for adding a method as an common play prev handler.""" + """ + Decorator for adding a method to handle requests to skip backward. + """ def real_decorator(func): # Store the flag inside the function @@ -58,7 +62,9 @@ def real_decorator(func): def ocp_next(): - """Decorator for adding a method as an common play next handler.""" + """ + Decorator for adding a method to handle requests to skip forward. + """ def real_decorator(func): # Store the flag inside the function @@ -72,7 +78,9 @@ def real_decorator(func): def ocp_pause(): - """Decorator for adding a method as an common play pause handler.""" + """ + Decorator for adding a method to handle requests to pause playback. + """ def real_decorator(func): # Store the flag inside the function @@ -86,7 +94,9 @@ def real_decorator(func): def ocp_resume(): - """Decorator for adding a method as an common play resume handler.""" + """ + Decorator for adding a method to handle requests to resume playback. + """ def real_decorator(func): # Store the flag inside the function @@ -100,7 +110,9 @@ def real_decorator(func): def ocp_featured_media(): - """Decorator for adding a method as an common play search handler.""" + """ + Decorator for adding a method to handle requests to provide featured media. + """ def real_decorator(func): # Store the flag inside the function @@ -114,8 +126,9 @@ def real_decorator(func): try: - from ovos_plugin_common_play.ocp.status import MediaType, PlayerState, MediaState, MatchConfidence, \ - PlaybackType, PlaybackMode, LoopState, TrackState + from ovos_plugin_common_play.ocp.status import MediaType, PlayerState, \ + MediaState, MatchConfidence, PlaybackType, PlaybackMode, LoopState, \ + TrackState except ImportError: # TODO - manually keep these in sync as needed diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index bd769880..7dae3460 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -49,8 +49,8 @@ from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties from ovos_utils.sound import play_acknowledge_sound +from ovos_utils import classproperty -from ovos_workshop.decorators import classproperty from ovos_workshop.decorators.killable import AbortEvent from ovos_workshop.decorators.killable import killable_event, \ AbortQuestion diff --git a/test/unittests/test_decorators.py b/test/unittests/test_decorators.py index bf22caa4..18fec9bf 100644 --- a/test/unittests/test_decorators.py +++ b/test/unittests/test_decorators.py @@ -1,12 +1,89 @@ import json import unittest from os.path import dirname +from unittest.mock import Mock from time import sleep from ovos_workshop.skill_launcher import SkillLoader from ovos_utils.messagebus import FakeBus, Message -from ovos_workshop.skills.mycroft_skill import is_classic_core + +class TestDecorators(unittest.TestCase): + def test_adds_context(self): + from ovos_workshop.decorators import adds_context + # TODO + + def test_removes_context(self): + from ovos_workshop.decorators import removes_context + # TODO + + def test_intent_handler(self): + from ovos_workshop.decorators import intent_handler + mock_intent = Mock() + called = False + + @intent_handler(mock_intent) + @intent_handler("test_intent") + def test_handler(): + nonlocal called + called = True + + self.assertEqual(test_handler.intents, ["test_intent", mock_intent]) + self.assertFalse(called) + + def test_resting_screen_handler(self): + from ovos_workshop.decorators import resting_screen_handler + called = False + + @resting_screen_handler("test_homescreen") + def show_homescreen(): + nonlocal called + called = True + + self.assertEqual(show_homescreen.resting_handler, "test_homescreen") + self.assertFalse(called) + + def test_skill_api_method(self): + from ovos_workshop.decorators import skill_api_method + called = False + + @skill_api_method + def api_method(): + nonlocal called + called = True + + self.assertTrue(api_method.api_method) + self.assertFalse(called) + + def test_converse_handler(self): + from ovos_workshop.decorators import converse_handler + called = False + + @converse_handler + def handle_converse(): + nonlocal called + called = True + + self.assertTrue(handle_converse.converse) + self.assertFalse(called) + + def test_fallback_handler(self): + from ovos_workshop.decorators import fallback_handler + called = False + + @fallback_handler() + def medium_prio_fallback(): + nonlocal called + called = True + + @fallback_handler(1) + def high_prio_fallback(): + nonlocal called + called = True + + self.assertEqual(medium_prio_fallback.fallback_priority, 50) + self.assertEqual(high_prio_fallback.fallback_priority, 1) + self.assertFalse(called) class TestKillableIntents(unittest.TestCase): @@ -208,22 +285,126 @@ def test_developer_stop_msg(self): sleep(2) self.assertTrue(self.bus.emitted_msgs == []) + def test_killable_event(self): + from ovos_workshop.decorators.killable import killable_event + # TODO -class TestConverse(unittest.TestCase): - # TODO - pass +class TestLayers(unittest.TestCase): + def test_dig_for_skill(self): + from ovos_workshop.decorators.layers import dig_for_skill + # TODO -class TestFallbackHandler(unittest.TestCase): - # TODO - pass + def test_enables_layer(self): + from ovos_workshop.decorators.layers import enables_layer + # TODO + def test_disables_layer(self): + from ovos_workshop.decorators.layers import disables_layer + # TODO -class TestLayers(unittest.TestCase): - # TODO - pass + def test_replaces_layer(self): + from ovos_workshop.decorators.layers import replaces_layer + # TODO + + def test_removes_layer(self): + from ovos_workshop.decorators.layers import removes_layer + # TODO + + def test_resets_layers(self): + from ovos_workshop.decorators.layers import resets_layers + # TODO + + def test_layer_intent(self): + from ovos_workshop.decorators.layers import layer_intent + # TODO + + def test_intent_layers(self): + from ovos_workshop.decorators.layers import IntentLayers + # TODO class TestOCP(unittest.TestCase): - # TODO - pass + def test_ocp_search(self): + from ovos_workshop.decorators.ocp import ocp_search + called = False + + @ocp_search() + def test_search(): + nonlocal called + called = True + + self.assertTrue(test_search.is_ocp_search_handler) + self.assertFalse(called) + + def test_ocp_play(self): + from ovos_workshop.decorators.ocp import ocp_play + called = False + + @ocp_play() + def test_play(): + nonlocal called + called = True + + self.assertTrue(test_play.is_ocp_playback_handler) + self.assertFalse(called) + + def test_ocp_previous(self): + from ovos_workshop.decorators.ocp import ocp_previous + called = False + + @ocp_previous() + def test_previous(): + nonlocal called + called = True + + self.assertTrue(test_previous.is_ocp_prev_handler) + self.assertFalse(called) + + def test_ocp_next(self): + from ovos_workshop.decorators.ocp import ocp_next + called = False + + @ocp_next() + def test_next(): + nonlocal called + called = True + + self.assertTrue(test_next.is_ocp_next_handler) + self.assertFalse(called) + + def test_ocp_pause(self): + from ovos_workshop.decorators.ocp import ocp_pause + called = False + + @ocp_pause() + def test_pause(): + nonlocal called + called = True + + self.assertTrue(test_pause.is_ocp_pause_handler) + self.assertFalse(called) + + def test_ocp_resume(self): + from ovos_workshop.decorators.ocp import ocp_resume + called = False + + @ocp_resume() + def test_resume(): + nonlocal called + called = True + + self.assertTrue(test_resume.is_ocp_resume_handler) + self.assertFalse(called) + + def test_ocp_featured_media(self): + from ovos_workshop.decorators.ocp import ocp_featured_media + called = False + + @ocp_featured_media() + def test_featured_media(): + nonlocal called + called = True + + self.assertTrue(test_featured_media.is_ocp_featured_handler) + self.assertFalse(called) From 985147a1473b176057635a57058e4da556e48771 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 7 Jul 2023 03:34:44 +0000 Subject: [PATCH 106/154] Increment Version to 0.0.12a35 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index c87eb66b..36ba0515 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 34 +VERSION_ALPHA = 35 # END_VERSION_BLOCK From 3fd73dc6e3eb9b1e9e096a56d40fda6a64b70072 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 7 Jul 2023 03:35:21 +0000 Subject: [PATCH 107/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2924d046..dbc4d4fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a34](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a34) (2023-07-06) +## [0.0.12a35](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a35) (2023-07-07) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a33...0.0.12a34) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a34...0.0.12a35) + +**Merged pull requests:** + +- Decorator module tests, docstrings, and annotations [\#107](https://github.com/OpenVoiceOS/OVOS-workshop/pull/107) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a34](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a34) (2023-07-06) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a33...V0.0.12a34) **Merged pull requests:** From 2d5c1d7895fd25e13288ca8409be54f2c72b2d50 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:09:45 -0700 Subject: [PATCH 108/154] Skills module tests, docstrings, and annotations (#108) --- .github/workflows/unit_tests.yml | 5 +- ovos_workshop/skills/active.py | 5 +- ovos_workshop/skills/auto_translatable.py | 97 +- ovos_workshop/skills/base.py | 1324 ++++++++++------- ovos_workshop/skills/decorators/__init__.py | 2 + ovos_workshop/skills/decorators/converse.py | 2 + .../skills/decorators/fallback_handler.py | 2 + ovos_workshop/skills/decorators/killable.py | 2 + ovos_workshop/skills/decorators/layers.py | 2 + ovos_workshop/skills/decorators/ocp.py | 2 + ovos_workshop/skills/fallback.py | 307 ++-- ovos_workshop/skills/idle_display_skill.py | 119 +- ovos_workshop/skills/intent_provider.py | 9 +- ovos_workshop/skills/layers.py | 2 + ovos_workshop/skills/mycroft_skill.py | 185 ++- ovos_workshop/skills/ovos.py | 208 ++- requirements/test.txt | 5 + .../skills/gui/ui/test.qml => __init__.py} | 0 .../vocab/en-us/test.intent => __init__.py} | 0 test/unittests/skills/test_active.py | 28 + .../skills/test_auto_translatable.py | 45 + test/unittests/skills/test_base.py | 407 +++++ test/unittests/skills/test_fallback_skill.py | 235 ++- .../gui/ui/test.qml} | 0 .../skills/test_idle_display_skill.py | 14 + .../skills/test_mycroft_skill/__init__.py | 0 .../intent_file/vocab/en-us/test.intent | 0 .../intent_file/vocab/en-us/test_ent.entity | 0 .../locale/en-us/turn_off2_test.voc | 0 .../locale/en-us/turn_off_test.voc | 0 .../skills/{ => test_mycroft_skill}/mocks.py | 0 .../test_mycroft_skill.py | 8 +- .../test_mycroft_skill_get_response.py | 0 .../test_skill/__init__.py | 0 .../dialog/en-us/what do you want.dialog | 0 .../in-dialog/dialog/en-us/good_things.list | 0 .../in-dialog/dialog/en-us/named_things.value | 0 .../in-dialog/dialog/en-us/test.template | 0 .../in-locale/locale/de-de/good_things.list | 0 .../in-locale/locale/de-de/named_things.value | 0 .../in-locale/locale/de-de/test.template | 0 .../in-locale/locale/en-us/good_things.list | 0 .../in-locale/locale/en-us/named_things.value | 0 .../in-locale/locale/en-us/not_in_german.list | 0 .../in-locale/locale/en-us/test.template | 0 .../test_ovos.py} | 144 +- test/unittests/test_abstract_app.py | 14 + test/unittests/test_skill.py | 45 +- test/unittests/test_skill_launcher.py | 6 +- 49 files changed, 2132 insertions(+), 1092 deletions(-) create mode 100644 requirements/test.txt rename test/{unittests/skills/gui/ui/test.qml => __init__.py} (100%) rename test/unittests/{skills/intent_file/vocab/en-us/test.intent => __init__.py} (100%) create mode 100644 test/unittests/skills/test_active.py create mode 100644 test/unittests/skills/test_auto_translatable.py create mode 100644 test/unittests/skills/test_base.py rename test/unittests/skills/{intent_file/vocab/en-us/test_ent.entity => test_gui/gui/ui/test.qml} (100%) create mode 100644 test/unittests/skills/test_idle_display_skill.py create mode 100644 test/unittests/skills/test_mycroft_skill/__init__.py create mode 100644 test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent create mode 100644 test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity rename test/unittests/skills/{ => test_mycroft_skill}/locale/en-us/turn_off2_test.voc (100%) rename test/unittests/skills/{ => test_mycroft_skill}/locale/en-us/turn_off_test.voc (100%) rename test/unittests/skills/{ => test_mycroft_skill}/mocks.py (100%) rename test/unittests/skills/{ => test_mycroft_skill}/test_mycroft_skill.py (99%) rename test/unittests/skills/{ => test_mycroft_skill}/test_mycroft_skill_get_response.py (100%) rename test/unittests/skills/{ => test_mycroft_skill}/test_skill/__init__.py (100%) rename test/unittests/skills/{ => test_mycroft_skill}/test_skill/dialog/en-us/what do you want.dialog (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-dialog/dialog/en-us/good_things.list (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-dialog/dialog/en-us/named_things.value (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-dialog/dialog/en-us/test.template (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/de-de/good_things.list (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/de-de/named_things.value (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/de-de/test.template (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/en-us/good_things.list (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/en-us/named_things.value (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/en-us/not_in_german.list (100%) rename test/unittests/skills/{ => test_mycroft_skill}/translate/in-locale/locale/en-us/test.template (100%) rename test/unittests/{test_skill_classes.py => skills/test_ovos.py} (50%) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 91600b7d..22c6eac0 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -49,12 +49,11 @@ jobs: python -m pip install build wheel - name: Install ovos workshop run: | - pip install . + pip install -e . - name: Install test dependencies run: | sudo apt install libssl-dev libfann-dev portaudio19-dev libpulse-dev - pip install ovos-core>=0.0.6a17 - pip install pytest pytest-timeout pytest-cov adapt-parser~=0.5 + pip install -r requirements/test.txt - name: Run unittests run: | pytest --cov=ovos_workshop --cov-report xml test/unittests diff --git a/ovos_workshop/skills/active.py b/ovos_workshop/skills/active.py index fd5b132e..325cd22c 100644 --- a/ovos_workshop/skills/active.py +++ b/ovos_workshop/skills/active.py @@ -9,8 +9,9 @@ def bind(self, bus): self.make_active() def handle_skill_deactivated(self, message=None): - """ skill is always in active skill list, - ie, converse is always called """ + """ + skill is always in active skill list, ie, converse is always called + """ self.make_active() diff --git a/ovos_workshop/skills/auto_translatable.py b/ovos_workshop/skills/auto_translatable.py index 0e8b0c05..0f29c39a 100644 --- a/ovos_workshop/skills/auto_translatable.py +++ b/ovos_workshop/skills/auto_translatable.py @@ -1,3 +1,5 @@ +from abc import ABC + from ovos_config import Configuration from ovos_plugin_manager.language import OVOSLangDetectionFactory, OVOSLangTranslationFactory from ovos_utils import get_handler_name @@ -5,53 +7,64 @@ from ovos_workshop.resource_files import SkillResources from ovos_workshop.skills.common_query_skill import CommonQuerySkill -from ovos_workshop.skills.ovos import OVOSSkill, OVOSFallbackSkill +from ovos_workshop.skills.ovos import OVOSSkill +from ovos_workshop.skills.fallback import FallbackSkillV2 class UniversalSkill(OVOSSkill): - ''' Skill that auto translates input/output from any language + """ + Skill that auto translates input/output from any language intent handlers are ensured to receive utterances in self.internal_language intent handlers are expected to produce utterances in self.internal_language - self.speak will always translate utterances from self.internal_lang to self.lang + self.speak will always translate utterances from + self.internal_lang to self.lang NOTE: self.lang reflects the original query language but received utterances are always in self.internal_language - ''' + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.lang_detector = OVOSLangDetectionFactory.create() self.translator = OVOSLangTranslationFactory.create() - self.internal_language = None # the skill internally only works in this language - self.translate_tags = True # __tags__ private value will be translated (adapt entities) - self.translate_keys = ["utterance", "utterances"] # keys added here will have values translated in message.data + # the skill internally only works in this language + self.internal_language = None + # __tags__ private value will be translated (adapt entities) + self.translate_tags = True + # keys added here will have values translated in message.data + self.translate_keys = ["utterance", "utterances"] - # autodetect will detect the lang of the utterance regardless of what has been reported - # to test just type in the cli in another language and watch answers still coming + # autodetect will detect the lang of the utterance regardless of what + # has been reported to test just type in the cli in another language + # and watch answers still coming self.autodetect = False # TODO from mycroft.conf if self.internal_language is None: lang = Configuration().get("lang", "en-us") - LOG.warning(f"UniversalSkill are expected to specify their internal_language, casting to {lang}") + LOG.warning(f"UniversalSkill are expected to specify their " + f"internal_language, casting to {lang}") self.internal_language = lang def _load_lang(self, root_directory=None, lang=None): - """unlike base skill class all resources are in self.internal_language by default - instead of self.lang (which comes from message) + """ + unlike base skill class all resources are in self.internal_language by + default instead of self.lang (which comes from message) this ensures things like self.dialog_render reflect self.internal_lang """ lang = lang or self.internal_language # self.lang in base class root_directory = root_directory or self.res_dir if lang not in self._lang_resources: - self._lang_resources[lang] = SkillResources(root_directory, lang, skill_id=self.skill_id) + self._lang_resources[lang] = SkillResources(root_directory, lang, + skill_id=self.skill_id) return self._lang_resources[lang] def detect_language(self, utterance): try: return self.lang_detector.detect(utterance) - except: + except Exception as e: + LOG.error(e) # self.lang to account for lang defined in message return self.lang.split("-")[0] @@ -61,7 +74,8 @@ def translate_utterance(self, text, target_lang, sauce_lang=None): else: sauce_lang = sauce_lang or self.detect_language(text) if sauce_lang.split("-")[0] != target_lang: - translated = self.translator.translate(text, source=sauce_lang, target=target_lang) + translated = self.translator.translate(text, source=sauce_lang, + target=target_lang) LOG.info("translated " + text + " to " + translated) return translated return text @@ -76,11 +90,13 @@ def _translate_message(self, message): return message translation_data = {"original": {}, "translated": {}, - "source_lang": sauce_lang, "internal_lang": self.internal_language} + "source_lang": sauce_lang, + "internal_lang": self.internal_language} def _do_tx(thing): if isinstance(thing, str): - thing = self.translate_utterance(thing, target_lang=out_lang, sauce_lang=sauce_lang) + thing = self.translate_utterance(thing, target_lang=out_lang, + sauce_lang=sauce_lang) elif isinstance(thing, list): thing = [_do_tx(t) for t in thing] elif isinstance(thing, dict): @@ -90,16 +106,19 @@ def _do_tx(thing): for key in self.translate_keys: if key in message.data: translation_data["original"][key] = message.data[key] - translation_data["translated"][key] = message.data[key] = _do_tx(message.data[key]) + translation_data["translated"][key] = message.data[key] = \ + _do_tx(message.data[key]) # special case if self.translate_tags: translation_data["original"]["__tags__"] = message.data["__tags__"] for idx, token in enumerate(message.data["__tags__"]): - message.data["__tags__"][idx] = self.translate_utterance(token.get("key", ""), - target_lang=out_lang, - sauce_lang=sauce_lang) - translation_data["translated"]["__tags__"] = message.data["__tags__"] + message.data["__tags__"][idx] = \ + self.translate_utterance(token.get("key", ""), + target_lang=out_lang, + sauce_lang=sauce_lang) + translation_data["translated"]["__tags__"] = \ + message.data["__tags__"] message.context["translation_data"] = translation_data return message @@ -138,18 +157,20 @@ def speak(self, utterance, *args, **kwargs): super().speak(utterance, *args, **kwargs) -class UniversalFallback(UniversalSkill, OVOSFallbackSkill): - ''' Fallback Skill that auto translates input/output from any language +class UniversalFallback(UniversalSkill, FallbackSkillV2): + """ + Fallback Skill that auto translates input/output from any language - fallback handlers are ensured to receive utterances in self.internal_language - fallback handlers are expected to produce utterances in self.internal_language + fallback handlers are ensured to receive utterances and expected to produce + responses in self.internal_language - self.speak will always translate utterances from self.internal_lang to self.lang + self.speak will always translate utterances from + self.internal_lang to self.lang NOTE: self.lang reflects the original query language but received utterances are always in self.internal_language - ''' + """ def create_universal_fallback_handler(self, handler): def universal_fallback_handler(message): @@ -165,22 +186,25 @@ def universal_fallback_handler(message): def register_fallback(self, handler, priority): handler = self.create_universal_fallback_handler(handler) - super().register_fallback(handler, priority) + FallbackSkillV2.register_fallback(self, handler, priority) -class UniversalCommonQuerySkill(UniversalSkill, CommonQuerySkill): - ''' CommonQuerySkill that auto translates input/output from any language +class UniversalCommonQuerySkill(UniversalSkill, CommonQuerySkill, ABC): + """ + CommonQuerySkill that auto translates input/output from any language - CQS_match_query_phrase and CQS_action are ensured to received phrase in self.internal_language + CQS_match_query_phrase and CQS_action are ensured to received phrase in + self.internal_language - CQS_match_query_phrase is assumed to return a response in self.internal_lang - it will be translated back before speaking + CQS_match_query_phrase is assumed to return a response in self.internal_lang + it will be translated back before speaking - self.speak will always translate utterances from self.internal_lang to self.lang + self.speak will always translate utterances from + self.internal_lang to self.lang NOTE: self.lang reflects the original query language but received utterances are always in self.internal_language - ''' + """ def __handle_query_action(self, message): """Message handler for question:action. @@ -220,4 +244,3 @@ def __get_cq(self, search_phrase): def remove_noise(self, phrase, lang=None): """remove noise to produce essence of question""" return super().remove_noise(phrase, self.internal_language) - diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 7dae3460..91d0079e 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -13,6 +13,7 @@ # limitations under the License. # """Common functionality relating to the implementation of mycroft skills.""" +import datetime import re import sys import time @@ -21,10 +22,11 @@ from hashlib import md5 from inspect import signature from itertools import chain -from os.path import join, abspath, dirname, basename, isfile, isdir +from os.path import join, abspath, dirname, basename, isfile from threading import Event -from typing import List +from typing import List, Optional, Dict, Callable, Union +from ovos_bus_client import MessageBusClient from ovos_bus_client.session import SessionManager from json_database import JsonStorage from lingua_franca.format import pronounce_number, join_list @@ -34,17 +36,19 @@ from ovos_config.config import Configuration from ovos_config.locations import get_xdg_config_save_path from ovos_utils import camel_case_split -from ovos_utils.dialog import get_dialog +from ovos_utils.dialog import get_dialog, MustacheDialogRenderer from ovos_utils.enclosure.api import EnclosureAPI -from ovos_utils.events import EventSchedulerInterface +from ovos_utils.events import EventContainer, EventSchedulerInterface from ovos_utils.file_utils import FileWatcher from ovos_utils.gui import GUIInterface, get_ui_directories from ovos_utils.intents import ConverseTracker from ovos_utils.intents import Intent, IntentBuilder -from ovos_utils.intents.intent_service_interface import munge_regex, munge_intent_parser, IntentServiceInterface +from ovos_utils.intents.intent_service_interface import munge_regex, \ + munge_intent_parser, IntentServiceInterface from ovos_utils.json_helper import merge_dict -from ovos_utils.log import LOG, deprecated -from ovos_utils.messagebus import get_handler_name, create_wrapper, EventContainer, get_message_lang +from ovos_utils.log import LOG, deprecated, log_deprecation +from ovos_utils.messagebus import get_handler_name, create_wrapper, \ + get_message_lang from ovos_utils.parse import match_one from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills import get_non_properties @@ -63,31 +67,34 @@ # backwards compat alias class SkillNetworkRequirements(RuntimeRequirements): def __init__(self, *args, **kwargs): - LOG.warning("SkillNetworkRequirements has been renamed to RuntimeRequirements\n" - "from ovos_utils.process_utils import RuntimeRequirements") + log_deprecation("Replace with " + "`ovos_utils.process_utils.RuntimeRequirements`", + "0.1.0") super().__init__(*args, **kwargs) -def is_classic_core(): - """ Check if the current core is the classic mycroft-core """ +def is_classic_core() -> bool: + """ + Check if the current core is the classic mycroft-core + """ try: from mycroft.version import OVOS_VERSION_STR return False # ovos-core except ImportError: try: + log_deprecation("Support for `mycroft_core` will be deprecated", + "0.1.0") from mycroft.version import CORE_VERSION_STR return True # mycroft-core except ImportError: return False # standalone -def simple_trace(stack_trace): - """Generate a simplified traceback. - - Args: - stack_trace: Stack trace to simplify - - Returns: (str) Simplified stack trace. +def simple_trace(stack_trace: List[str]) -> str: + """ + Generate a simplified traceback. + @param stack_trace: Formatted stack trace (each string ends with \n) + @return: Stack trace with any empty lines removed and last line removed """ stack_trace = stack_trace[:-1] tb = 'Traceback:\n' @@ -131,50 +138,64 @@ class BaseSkill: bus (MycroftWebsocketClient): Optional bus connection """ - def __init__(self, name=None, bus=None, resources_dir=None, - settings: JsonStorage = None, - gui=None, enable_settings_manager=True, - skill_id=""): + def __init__(self, name: Optional[str] = None, + bus: Optional[MessageBusClient] = None, + resources_dir: Optional[str] = None, + settings: Optional[JsonStorage] = None, + gui: Optional[GUIInterface] = None, + enable_settings_manager: bool = True, + skill_id: str = ""): + """ + Create an OVOSSkill object. + @param name: DEPRECATED skill_name + @param bus: MessageBusClient to bind to skill + @param resources_dir: optional root resource directory (else defaults to + skill `root_dir` + @param settings: Optional settings object, else defined in skill config + path + @param gui: Optional SkillGUI, else one is initialized + @param enable_settings_manager: if True, enables a SettingsManager for + this skill to manage default settings and backend sync + @param skill_id: Unique ID for this skill + """ self.log = LOG # a dedicated namespace will be assigned in _startup self._enable_settings_manager = enable_settings_manager self._init_event = Event() self.name = name or self.__class__.__name__ self.resting_name = None - self.skill_id = skill_id # will be set by SkillLoader, guaranteed unique + self.skill_id = skill_id # set by SkillLoader, guaranteed unique self._settings_meta = None # DEPRECATED - backwards compat only self.settings_manager = None - # Get directory of skill - #: Member variable containing the absolute path of the skill's root - #: directory. E.g. $XDG_DATA_HOME/mycroft/skills/my-skill.me/ + # Get directory of skill source (__init__.py) self.root_dir = dirname(abspath(sys.modules[self.__module__].__file__)) self.res_dir = resources_dir or self.root_dir self.gui = gui - self._bus = bus self._enclosure = EnclosureAPI() - #: Mycroft global configuration. (dict) - self.config_core = Configuration() + # Core configuration + self.config_core: Configuration = Configuration() self._settings = None self._initial_settings = settings or dict() self._settings_watchdog = None - #: Set to register a callback method that will be called every time - #: the skills settings are updated. The referenced method should - #: include any logic needed to handle the updated settings. + # Override to register a callback method that will be called every time + # the skill's settings are updated. The referenced method should + # include any logic needed to handle the updated settings. self.settings_change_callback = None # fully initialized when self.skill_id is set self._file_system = None - self.log = LOG - self.reload_skill = True #: allow reloading (default True) + self.reload_skill = True # allow reloading (default True) self.events = EventContainer(bus) + + # Cached voc file contents self._voc_cache = {} # loaded lang file resources @@ -185,7 +206,7 @@ def __init__(self, name=None, bus=None, resources_dir=None, self.intent_service = IntentServiceInterface() # Skill Public API - self.public_api = {} + self.public_api: Dict[str, dict] = {} self.__original_converse = self.converse @@ -193,49 +214,51 @@ def __init__(self, name=None, bus=None, resources_dir=None, if self.skill_id and self.bus: self._startup(self.bus, self.skill_id) - # classproperty not present in mycroft-core @classproperty - def runtime_requirements(self): - """ skill developers should override this if they do not require connectivity - - some examples: - - IOT skill that controls skills via LAN could return: - scans_on_init = True - RuntimeRequirements(internet_before_load=False, - network_before_load=scans_on_init, - requires_internet=False, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=False) - - online search skill with a local cache: - has_cache = False - RuntimeRequirements(internet_before_load=not has_cache, - network_before_load=not has_cache, - requires_internet=True, - requires_network=True, - no_internet_fallback=True, - no_network_fallback=True) - - a fully offline skill: - RuntimeRequirements(internet_before_load=False, - network_before_load=False, - requires_internet=False, - requires_network=False, - no_internet_fallback=True, - no_network_fallback=True) + def runtime_requirements(self) -> RuntimeRequirements: + """ + Override to specify what a skill expects to be available at init and at + runtime. Default will assume network and internet are required and GUI + is not required for backwards-compat. + + some examples: + + IOT skill that controls skills via LAN could return: + scans_on_init = True + RuntimeRequirements(internet_before_load=False, + network_before_load=scans_on_init, + requires_internet=False, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=False) + + online search skill with a local cache: + has_cache = False + RuntimeRequirements(internet_before_load=not has_cache, + network_before_load=not has_cache, + requires_internet=True, + requires_network=True, + no_internet_fallback=True, + no_network_fallback=True) + + a fully offline skill: + RuntimeRequirements(internet_before_load=False, + network_before_load=False, + requires_internet=False, + requires_network=False, + no_internet_fallback=True, + no_network_fallback=True) """ return RuntimeRequirements() @classproperty - def network_requirements(self): + def network_requirements(self) -> RuntimeRequirements: LOG.warning("network_requirements renamed to runtime_requirements, " "will be removed in ovos-core 0.0.8") return self.runtime_requirements @property - def voc_match_cache(self): + def voc_match_cache(self) -> Dict[str, List[str]]: """ Backwards-compatible accessor method for vocab cache @return: dict vocab resources to parsed resources @@ -249,166 +272,48 @@ def voc_match_cache(self, val): if isinstance(val, dict): self._voc_cache = val - # property not present in mycroft-core + # not a property in mycroft-core @property - def _is_fully_initialized(self): - """Determines if the skill has been fully loaded and setup. - When True all data has been loaded and all internal state and events setup""" - return self._init_event.is_set() - - # method not present in mycroft-core - def _handle_first_run(self): - """The very first time a skill is run, speak the intro.""" - intro = self.get_intro_message() - if intro: - # supports .dialog files for easy localization - # when .dialog does not exist, the text is spoken - # it is backwards compatible - self.speak_dialog(intro) - - # method not present in mycroft-core - def _check_for_first_run(self): - """Determine if its the very first time a skill is run.""" - first_run = self.settings.get("__mycroft_skill_firstrun", True) - if first_run: - self.log.info("First run of " + self.skill_id) - self._handle_first_run() - self.settings["__mycroft_skill_firstrun"] = False - self.settings.store() - - # method not present in mycroft-core - def _startup(self, bus, skill_id=""): - """Startup the skill. - - This connects the skill to the messagebus, loads vocabularies and - data files and in the end calls the skill creator's "intialize" code. - - Arguments: - bus: Mycroft Messagebus connection object. - skill_id (str): need to be unique, by default is set from skill path - but skill loader can override this + def _is_fully_initialized(self) -> bool: """ - if self._is_fully_initialized: - self.log.warning(f"Tried to initialize {self.skill_id} multiple times, ignoring") - return - - # NOTE: this method is called by SkillLoader - # it is private to make it clear to skill devs they should not touch it - try: - # set the skill_id - self.skill_id = skill_id or basename(self.root_dir) - self.intent_service.set_id(self.skill_id) - self.event_scheduler.set_id(self.skill_id) - self.enclosure.set_id(self.skill_id) - - # initialize anything that depends on skill_id - self.log = LOG.create_logger(self.skill_id) - self._init_settings() - - # initialize anything that depends on the messagebus - self.bind(bus) - if not self.gui: - self._init_skill_gui() - if self._enable_settings_manager: - self._init_settings_manager() - self.load_data_files() - self._register_decorated() - self.register_resting_screen() - - # run skill developer initialization code - self.initialize() - self._check_for_first_run() - self._init_event.set() - except Exception as e: - self.log.exception('Skill initialization failed') - # If an exception occurs, attempt to clean up the skill - try: - self.default_shutdown() - except Exception as e2: - pass - raise e - - def _init_settings(self): - """Setup skill settings.""" - self.log.debug(f"initializing skill settings for {self.skill_id}") - - # NOTE: lock is disabled due to usage of deepcopy and to allow json serialization - self._settings = JsonStorage(self._settings_path, disable_lock=True) - if self._initial_settings and not self._is_fully_initialized: - self.log.warning("Copying default settings values defined in __init__ \n" - f"to correct this add kwargs __init__(bus=None, skill_id='') " - f"to skill class {self.__class__.__name__}") - for k, v in self._initial_settings.items(): - if k not in self._settings: - self._settings[k] = v - self._initial_settings = copy(self.settings) - - self._start_filewatcher() - - # method not in mycroft-core - def _init_skill_gui(self): - try: - self.gui = SkillGUI(self) - self.gui.setup_default_handlers() - except ImportError: - self.gui = GUIInterface(self.skill_id) - if self.bus: - self.gui.set_bus(self.bus) - - # method not in mycroft-core - def _init_settings_manager(self): - self.settings_manager = SkillSettingsManager(self) - - # method not present in mycroft-core - def _start_filewatcher(self): - if self._settings_watchdog is None and isfile(self._settings.path): - self._settings_watchdog = FileWatcher([self._settings.path], - callback=self._handle_settings_file_change, - ignore_creation=True) - - # method not present in mycroft-core - def _upload_settings(self): - if self.settings_manager and self.config_core.get("skills", {}).get("sync2way"): - # upload new settings to backend - generate = self.config_core.get("skills", {}).get("autogen_meta", True) - self.settings_manager.upload(generate) # this will check global sync flag - if generate: - # update settingsmeta file on disk - self.settings_manager.save_meta() - - # method not present in mycroft-core - def _handle_settings_file_change(self, path): - if self._settings: - self._settings.reload() - if self.settings_change_callback: - try: - self.settings_change_callback() - except: - self.log.exception("settings change callback failed, " - "file changes not handled!") - self._upload_settings() + Determines if the skill has been fully loaded and setup. + When True, all data has been loaded and all internal state + and events set up. + """ + return self._init_event.is_set() # not a property in mycroft-core @property - def _settings_path(self): - return join(get_xdg_config_save_path(), 'skills', self.skill_id, 'settings.json') + def _settings_path(self) -> str: + """ + Absolute file path of this skill's `settings.json` (file may not exist) + """ + return join(get_xdg_config_save_path(), 'skills', self.skill_id, + 'settings.json') # not a property in mycroft-core @property - def settings(self): + def settings(self) -> JsonStorage: + """ + Get settings specific to this skill + """ if self._settings is not None: return self._settings else: - self.log.warning('Skill not fully initialized. ' - 'Only default values can be set, no settings can be read or changed.' - f"to correct this add kwargs __init__(bus=None, skill_id='') " + self.log.warning('Skill not fully initialized. Only default values ' + 'can be set, no settings can be read or changed.' + f"to correct this add kwargs " + f"__init__(bus=None, skill_id='') " f"to skill class {self.__class__.__name__}") self.log.error(simple_trace(traceback.format_stack())) return self._initial_settings # not a property in mycroft-core @settings.setter - def settings(self, val): + def settings(self, val: dict): + """ + Update settings specific to this skill + """ assert isinstance(val, dict) # init method if self._settings is None: @@ -420,26 +325,34 @@ def settings(self, val): # not a property in mycroft-core @property - def dialog_renderer(self): + def dialog_renderer(self) -> Optional[MustacheDialogRenderer]: + """ + Get a dialog renderer for this skill. Language will be determined by + message history to match the language associated with the current + session or else from Configuration. + """ return self._resources.dialog_renderer @property - def enclosure(self): + def enclosure(self) -> EnclosureAPI: + """ + Get an EnclosureAPI object to interact with hardware + """ if self._enclosure: return self._enclosure else: self.log.warning('Skill not fully initialized.' - f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to correct this add kwargs " + f"__init__(bus=None, skill_id='') " f"to skill class {self.__class__.__name__}") self.log.error(simple_trace(traceback.format_stack())) raise Exception('Accessed MycroftSkill.enclosure in __init__') # not a property in mycroft-core @property - def file_system(self): - """ Filesystem access to skill specific folder. - - See mycroft.filesystem for details. + def file_system(self) -> FileSystemAccess: + """ + Get an object that provides managed access to a local Filesystem. """ if not self._file_system and self.skill_id: self._file_system = FileSystemAccess(join('skills', self.skill_id)) @@ -453,24 +366,40 @@ def file_system(self): raise Exception('Accessed MycroftSkill.file_system in __init__') @file_system.setter - def file_system(self, fs): - """Provided mainly for backwards compatibility with derivative MycroftSkill classes - Skills are advised against redefining the file system directory""" + def file_system(self, fs: FileSystemAccess): + """ + Provided mainly for backwards compatibility with derivative + MycroftSkill classes. Skills are advised against redefining the file + system directory. + @param fs: new FileSystemAccess object to use + """ + self.log.warning(f"Skill manually overriding file_system path to: " + f"{fs.path}") self._file_system = fs @property - def bus(self): + def bus(self) -> MessageBusClient: + """ + Get the MessageBusClient bound to this skill + """ if self._bus: return self._bus else: self.log.warning('Skill not fully initialized.' - f"to correct this add kwargs __init__(bus=None, skill_id='') " + f"to correct this add kwargs " + f"__init__(bus=None, skill_id='') " f"to skill class {self.__class__.__name__}") self.log.error(simple_trace(traceback.format_stack())) raise Exception('Accessed MycroftSkill.bus in __init__') @bus.setter - def bus(self, value): + def bus(self, value: MessageBusClient): + """ + Set the MessageBusClient bound to this skill. Note that setting this + after init may have unintended consequences as expected events might + not be registered. Call `bind` to connect a new MessageBusClient. + @param value: new MessageBusClient object + """ from ovos_bus_client import MessageBusClient from ovos_utils.messagebus import FakeBus if isinstance(value, (MessageBusClient, FakeBus)): @@ -479,31 +408,40 @@ def bus(self, value): raise TypeError(f"Expected a MessageBusClient, got: {type(value)}") @property - def location(self): - """Get the JSON data struction holding location information.""" + def location(self) -> dict: + """ + Get the JSON data struction holding location information. + """ # TODO: Allow Enclosure to override this for devices that - # contain a GPS. + # contain a GPS. return self.config_core.get('location') @property - def location_pretty(self): - """Get a more 'human' version of the location as a string.""" + def location_pretty(self) -> Optional[str]: + """ + Get a speakable city from the location config if available + """ loc = self.location if type(loc) is dict and loc['city']: return loc['city']['name'] return None @property - def location_timezone(self): - """Get the timezone code, such as 'America/Los_Angeles'""" + def location_timezone(self) -> Optional[str]: + """ + Get the timezone code, such as 'America/Los_Angeles' + """ loc = self.location if type(loc) is dict and loc['timezone']: return loc['timezone']['code'] return None @property - def lang(self): - """Get the current language.""" + def lang(self) -> str: + """ + Get the current language as a BCP-47 language code. This will consider + current session data if available, else Configuration. + """ lang = self._core_lang message = dig_for_message() if message: @@ -512,82 +450,266 @@ def lang(self): # property not present in mycroft-core @property - def _core_lang(self): - """Get the configured default language. - NOTE: this should be public, but since if a skill uses this it wont - work in regular mycroft-core it was made private!""" + def _core_lang(self) -> str: + """ + Get the configured default language as a BCP-47 language code. + + NOTE: this should be public, but since if a skill uses this it won't + work in regular mycroft-core it was made private! + """ return self.config_core.get("lang", "en-us").lower() # property not present in mycroft-core @property - def _secondary_langs(self): - """Get the configured secondary languages, mycroft is not - considered to be in these languages, but will load its resource - files. This provides initial support for multilingual input. A skill - may override this method to specify which languages intents are - registered in. - NOTE: this should be public, but since if a skill uses this it wont - work in regular mycroft-core it was made private!""" - return [l.lower() for l in self.config_core.get('secondary_langs', []) - if l != self._core_lang] + def _secondary_langs(self) -> List[str]: + """ + Get the configured secondary languages; resources will be loaded for + these languages to provide support for multilingual input, in addition + to `core_lang`. A skill may override this method to specify which + languages intents are registered in. + + NOTE: this should be public, but since if a skill uses this it won't + work in regular mycroft-core it was made private! + """ + return [lang.lower() for lang in self.config_core.get( + 'secondary_langs', []) if lang != self._core_lang] # property not present in mycroft-core @property - def _native_langs(self): - """Languages natively supported by core - ie, resource files available and explicitly supported - NOTE: this should be public, but since if a skill uses this it wont + def _native_langs(self) -> List[str]: + """ + Languages natively supported by this skill (ie, resource files available + and explicitly supported). This is equivalent to normalized + secondary_langs + core_lang. + + NOTE: this should be public, but since if a skill uses this it won't work in regular mycroft-core it was made private! """ - valid = set([l.lower() for l in self._secondary_langs - if '-' in l and l != self._core_lang] + [self._core_lang]) + valid = set([lang.lower() for lang in self._secondary_langs if '-' in + lang and lang != self._core_lang] + [self._core_lang]) return list(valid) # property not present in mycroft-core @property - def _alphanumeric_skill_id(self): - """skill id converted to only alphanumeric characters - Non alpha-numeric characters are converted to "_" + def _alphanumeric_skill_id(self) -> str: + """ + Skill id converted to only alphanumeric characters and "_". + Non alphanumeric characters are converted to "_" - NOTE: this should be public, but since if a skill uses this it wont + NOTE: this should be public, but since if a skill uses this it won't work in regular mycroft-core it was made private! - - Returns: - (str) String of letters """ return ''.join(c if c.isalnum() else '_' for c in str(self.skill_id)) # property not present in mycroft-core @property - def _resources(self): - """Instantiates a ResourceFileLocator instance when needed. - a new instance is always created to ensure self.lang - reflects the active language and not the default core language - NOTE: this should be public, but since if a skill uses this it wont + def _resources(self) -> SkillResources: + """ + Get a SkillResources object for the current language. Objects are + initialized for the current language as needed. + + NOTE: this should be public, but since if a skill uses this it won't work in regular mycroft-core it was made private! """ return self._load_lang(self.res_dir, self.lang) + # property not present in mycroft-core + @property + def _stop_is_implemented(self) -> bool: + """ + True if this skill implements a `stop` method + """ + return self.__class__.stop is not BaseSkill.stop + + # property not present in mycroft-core + @property + def _converse_is_implemented(self) -> bool: + """ + True if this skill implements a `converse` method + """ + return self.__class__.converse is not BaseSkill.converse or \ + self.__original_converse != self.converse + + # method not present in mycroft-core + def _handle_first_run(self): + """ + The very first time a skill is run, speak a provided intro_message. + """ + intro = self.get_intro_message() + if intro: + # supports .dialog files for easy localization + # when .dialog does not exist, the text is spoken + # it is backwards compatible + self.speak_dialog(intro) + + # method not present in mycroft-core + def _check_for_first_run(self): + """ + Determine if this is the very first time a skill is run by looking for + `__mycroft_skill_firstrun` in skill settings. + """ + first_run = self.settings.get("__mycroft_skill_firstrun", True) + if first_run: + self.log.info("First run of " + self.skill_id) + self._handle_first_run() + self.settings["__mycroft_skill_firstrun"] = False + self.settings.store() + + def _startup(self, bus: MessageBusClient, skill_id: str = ""): + """ + Startup the skill. Connects the skill to the messagebus, loads resources + and finally calls the skill's "intialize" method. + @param bus: MessageBusClient to bind to skill + @param skill_id: Unique skill identifier, defaults to skill path for + legacy skills and python entrypoints for modern skills + """ + if self._is_fully_initialized: + self.log.warning(f"Tried to initialize {self.skill_id} multiple " + f"times, ignoring") + return + + # NOTE: this method is called by SkillLoader + # it is private to make it clear to skill devs they should not touch it + try: + # set the skill_id + self.skill_id = skill_id or basename(self.root_dir) + self.intent_service.set_id(self.skill_id) + self.event_scheduler.set_id(self.skill_id) + self.enclosure.set_id(self.skill_id) + + # initialize anything that depends on skill_id + self.log = LOG.create_logger(self.skill_id) + self._init_settings() + + # initialize anything that depends on the messagebus + self.bind(bus) + if not self.gui: + self._init_skill_gui() + if self._enable_settings_manager: + self._init_settings_manager() + self.load_data_files() + self._register_decorated() + self.register_resting_screen() + + # run skill developer initialization code + self.initialize() + self._check_for_first_run() + self._init_event.set() + except Exception as e: + self.log.exception('Skill initialization failed') + # If an exception occurs, attempt to clean up the skill + try: + self.default_shutdown() + except Exception as e2: + LOG.debug(e2) + raise e + + def _init_settings(self): + """ + Set up skill settings. Defines settings in the specified file path, + handles any settings passed to skill init, and starts watching the + settings file for changes. + """ + self.log.debug(f"initializing skill settings for {self.skill_id}") + + # NOTE: lock is disabled due to usage of deepcopy and to allow json + # serialization + self._settings = JsonStorage(self._settings_path, disable_lock=True) + if self._initial_settings and not self._is_fully_initialized: + self.log.warning("Copying default settings values defined in " + "__init__ \nto correct this add kwargs " + "__init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + for k, v in self._initial_settings.items(): + if k not in self._settings: + self._settings[k] = v + self._initial_settings = copy(self.settings) + + self._start_filewatcher() + + def _init_skill_gui(self): + """ + Set up the SkillGUI for this skill and connect relevant bus events. + """ + self.gui = SkillGUI(self) + self.gui.setup_default_handlers() + + def _init_settings_manager(self): + """ + Set up the SkillSettingsManager for this skill. + """ + self.settings_manager = SkillSettingsManager(self) + + def _start_filewatcher(self): + """ + Start watching settings for file changes if settings file exists and + there isn't already a FileWatcher watching it + """ + if self._settings_watchdog is None and isfile(self._settings.path): + self._settings_watchdog = \ + FileWatcher([self._settings.path], + callback=self._handle_settings_file_change, + ignore_creation=True) + + # method not present in mycroft-core + def _upload_settings(self): + """ + Upload settings to a remote backend if configured. + """ + if self.settings_manager and self.config_core.get("skills", + {}).get("sync2way"): + # upload new settings to backend + generate = self.config_core.get("skills", {}).get("autogen_meta", + True) + # this will check global sync flag + self.settings_manager.upload(generate) + if generate: + # update settingsmeta file on disk + self.settings_manager.save_meta() + # method not present in mycroft-core - def _load_lang(self, root_directory=None, lang=None): - """Instantiates a ResourceFileLocator instance when needed. - a new instance is always created to ensure lang - reflects the active language and not the default core language - NOTE: this should be public, but since if a skill uses this it wont + def _handle_settings_file_change(self, path: str): + """ + Handle a FileWatcher notification that a file was changed. Reload + settings, call `self.settings_change_callback` if defined, and upload + changes if a backend is configured. + @param path: Modified file path + """ + if self._settings: + self._settings.reload() + if self.settings_change_callback: + try: + self.settings_change_callback() + except Exception as e: + self.log.exception("settings change callback failed, " + f"file changes not handled!: {e}") + self._upload_settings() + + # method not present in mycroft-core + def _load_lang(self, root_directory: Optional[str] = None, + lang: Optional[str] = None) -> SkillResources: + """ + Get a SkillResources object for this skill in the requested `lang` for + resource files in the requested `root_directory`. + @param root_directory: root path to find resources (default res_dir) + @param lang: language to get resources for (default self.lang) + @return: SkillResources object + + NOTE: this should be public, but since if a skill uses this it won't work in regular mycroft-core it was made private! """ lang = lang or self.lang root_directory = root_directory or self.res_dir if lang not in self._lang_resources: - self._lang_resources[lang] = SkillResources(root_directory, lang, skill_id=self.skill_id) + self._lang_resources[lang] = SkillResources(root_directory, lang, + skill_id=self.skill_id) return self._lang_resources[lang] - def bind(self, bus): - """Register messagebus emitter with skill. - - Args: - bus: Mycroft messagebus connection + def bind(self, bus: MessageBusClient): + """ + Register MessageBusClient with skill. + @param bus: MessageBusClient to bind to skill and internal objects """ if bus: self._bus = bus @@ -599,27 +721,28 @@ def bind(self, bus): self._register_public_api() if is_classic_core(): - # inject ovos exclusive features in vanila mycroft-core if possible - ## limited support for missing skill deactivated event + log_deprecation("Support for mycroft-core is deprecated", + "0.1.0") + # inject ovos exclusive features in vanilla mycroft-core + # if possible + # limited support for missing skill deactivated event # TODO - update ConverseTracker ConverseTracker.connect_bus(self.bus) # pull/1468 self.add_event("converse.skill.deactivated", - self._handle_skill_deactivated, speak_errors=False) + self._handle_skill_deactivated, + speak_errors=False) def _register_public_api(self): - """ Find and register api methods. - Api methods has been tagged with the api_method member, for each - method where this is found the method a message bus handler is - registered. - Finally create a handler for fetching the api info from any requesting - skill. + """ + Find and register API methods decorated with `@api_method` and create a + messagebus handler for fetching the api info if any handlers exist. """ - def wrap_method(func): - """Boiler plate for returning the response to the sender.""" + def wrap_method(fn): + """Boilerplate for returning the response to the sender.""" def wrapper(message): - result = func(*message.data['args'], **message.data['kwargs']) + result = fn(*message.data['args'], **message.data['kwargs']) message.context["skill_id"] = self.skill_id self.bus.emit(message.response(data={'result': result})) @@ -642,7 +765,8 @@ def wrapper(message): for key in self.public_api: if ('type' in self.public_api[key] and 'func' in self.public_api[key]): - self.log.debug(f"Adding api method: {self.public_api[key]['type']}") + self.log.debug(f"Adding api method: " + f"{self.public_api[key]['type']}") # remove the function member since it shouldn't be # reused and can't be sent over the messagebus @@ -654,44 +778,46 @@ def wrapper(message): self.add_event(f'{self.skill_id}.public_api', self._send_public_api, speak_errors=False) - # property not present in mycroft-core - @property - def _stop_is_implemented(self): - return self.__class__.stop is not BaseSkill.stop - - # property not present in mycroft-core - @property - def _converse_is_implemented(self): - return self.__class__.converse is not BaseSkill.converse or \ - self.__original_converse != self.converse - def _register_system_event_handlers(self): - """Add all events allowing the standard interaction with the Mycroft - system. + """ + Register default messagebus event handlers """ # Only register stop if it's been implemented if self._stop_is_implemented: - self.add_event('mycroft.stop', self.__handle_stop, speak_errors=False) - self.add_event('skill.converse.ping', self._handle_converse_ack, speak_errors=False) - self.add_event('skill.converse.request', self._handle_converse_request, speak_errors=False) - self.add_event(f"{self.skill_id}.activate", self.handle_activate, speak_errors=False) - self.add_event(f"{self.skill_id}.deactivate", self.handle_deactivate, speak_errors=False) - self.add_event("intent.service.skills.deactivated", self._handle_skill_deactivated, speak_errors=False) - self.add_event("intent.service.skills.activated", self._handle_skill_activated, speak_errors=False) - self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent, speak_errors=False) - self.add_event('mycroft.skill.disable_intent', self.handle_disable_intent, speak_errors=False) - self.add_event('mycroft.skill.set_cross_context', self.handle_set_cross_context, speak_errors=False) - self.add_event('mycroft.skill.remove_cross_context', self.handle_remove_cross_context, speak_errors=False) - self.add_event('mycroft.skills.settings.changed', self.handle_settings_change, speak_errors=False) - - def handle_settings_change(self, message): - """Update settings if the remote settings changes apply to this skill. + self.add_event('mycroft.stop', self.__handle_stop, + speak_errors=False) + self.add_event('skill.converse.ping', self._handle_converse_ack, + speak_errors=False) + self.add_event('skill.converse.request', self._handle_converse_request, + speak_errors=False) + self.add_event(f"{self.skill_id}.activate", self.handle_activate, + speak_errors=False) + self.add_event(f"{self.skill_id}.deactivate", self.handle_deactivate, + speak_errors=False) + self.add_event("intent.service.skills.deactivated", + self._handle_skill_deactivated, speak_errors=False) + self.add_event("intent.service.skills.activated", + self._handle_skill_activated, speak_errors=False) + self.add_event('mycroft.skill.enable_intent', self.handle_enable_intent, + speak_errors=False) + self.add_event('mycroft.skill.disable_intent', + self.handle_disable_intent, speak_errors=False) + self.add_event('mycroft.skill.set_cross_context', + self.handle_set_cross_context, speak_errors=False) + self.add_event('mycroft.skill.remove_cross_context', + self.handle_remove_cross_context, speak_errors=False) + self.add_event('mycroft.skills.settings.changed', + self.handle_settings_change, speak_errors=False) + + def handle_settings_change(self, message: Message): + """ + Update settings if a remote settings changes apply to this skill. The skill settings downloader uses a single API call to retrieve the - settings for all skills. This is done to limit the number API calls. + settings for all skills to limit the number API calls. A "mycroft.skills.settings.changed" event is emitted for each skill - that had their settings changed. Only update this skill's settings - if its remote settings were among those changed + with settings changes. Only update this skill's settings if its remote + settings were among those changed. """ remote_settings = message.data.get(self.skill_id) if remote_settings is not None: @@ -701,68 +827,84 @@ def handle_settings_change(self, message): if self.settings_change_callback is not None: try: self.settings_change_callback() - except: + except Exception as e: self.log.exception("settings change callback failed, " - "remote changes not handled!") + f"remote changes not handled!: {e}") self._start_filewatcher() def detach(self): + """ + Detach all intents for this skill from the intent_service. + """ for (name, _) in self.intent_service: name = f'{self.skill_id}:{name}' self.intent_service.detach_intent(name) def initialize(self): - """Perform any final setup needed for the skill. - - Invoked after the skill is fully constructed and registered with the - system. + """ + Legacy method overridden by skills to perform extra init after __init__. + Skills should now move any code in this method to `__init__`, after a + call to `super().__init__`. """ pass - def _send_public_api(self, message): - """Respond with the skill's public api.""" + def _send_public_api(self, message: Message): + """ + Respond with the skill's public api. + @param message: `{self.skill_id}.public_api` Message + """ message.context["skill_id"] = self.skill_id self.bus.emit(message.response(data=self.public_api)) - def get_intro_message(self): - """Get a message to speak on first load of the skill. - - Useful for post-install setup instructions. - - Returns: - str: message that will be spoken to the user + def get_intro_message(self) -> str: """ - return None + Override to return a string to speak on first run. i.e. for post-install + setup instructions. + """ + return "" # method not present in mycroft-core - def _handle_skill_activated(self, message): - """ intent service activated a skill - if it was this skill fire the skill activation event""" + def _handle_skill_activated(self, message: Message): + """ + Intent service activated a skill. If it was this skill, + emit a skill activation message. + @param message: `intent.service.skills.activated` Message + """ if message.data.get("skill_id") == self.skill_id: self.bus.emit(message.forward(f"{self.skill_id}.activate")) # method not present in mycroft-core - def handle_activate(self, message): - """ skill is now considered active by the intent service - converse method will be called, skills might want to prepare/resume + def handle_activate(self, message: Message): + """ + Called when this skill is considered active by the intent service; + converse method will be called with every utterance. + Override this method to do any optional preparation. + @param message: `{self.skill_id}.activate` Message """ # method not present in mycroft-core def _handle_skill_deactivated(self, message): - """ intent service deactivated a skill - if it was this skill fire the skill deactivation event""" + """ + Intent service deactivated a skill. If it was this skill, + emit a skill deactivation message. + @param message: `intent.service.skills.deactivated` Message + """ if message.data.get("skill_id") == self.skill_id: self.bus.emit(message.forward(f"{self.skill_id}.deactivate")) # method not present in mycroft-core def handle_deactivate(self, message): - """ skill is no longer considered active by the intent service - converse method will not be called, skills might want to reset state here + """ + Called when this skill is no longer considered active by the intent + service; converse method will not be called until skill is active again. + Override this method to do any optional cleanup. + @param message: `{self.skill_id}.deactivate` Message """ # named make_active in mycroft-core def _activate(self): - """Bump skill to active_skill list in intent_service. + """ + Mark this skill as active and push to the top of the active skills list. This enables converse method to be called even without skill being used in last 5 minutes. """ @@ -770,18 +912,21 @@ def _activate(self): if "skill_id" not in msg.context: msg.context["skill_id"] = self.skill_id - m1 = msg.forward("intent.service.skills.activate", data={"skill_id": self.skill_id}) + m1 = msg.forward("intent.service.skills.activate", + data={"skill_id": self.skill_id}) self.bus.emit(m1) # backwards compat with mycroft-core # TODO - remove soon - m2 = msg.forward("active_skill_request", data={"skill_id": self.skill_id}) + m2 = msg.forward("active_skill_request", + data={"skill_id": self.skill_id}) self.bus.emit(m2) # method not present in mycroft-core def _deactivate(self): - """remove skill from active_skill list in intent_service. - This stops converse method from being called + """ + Mark this skill as inactive and remove from the active skills list. + This stops converse method from being called. """ msg = dig_for_message() or Message("") if "skill_id" not in msg.context: @@ -789,10 +934,14 @@ def _deactivate(self): self.bus.emit(msg.forward(f"intent.service.skills.deactivate", data={"skill_id": self.skill_id})) - # method not present in mycroft-core - def _handle_converse_ack(self, message): - """Inform skills service if we want to handle converse. - individual skills may override the property self.converse_is_implemented""" + def _handle_converse_ack(self, message: Message): + """ + Inform skills service if we want to handle converse. Individual skills + may override the property self.converse_is_implemented to enable or + disable converse support. Note that this does not affect a skill's + `active` status. + @param message: `skill.converse.ping` Message + """ self.bus.emit(message.reply( "skill.converse.pong", data={"skill_id": self.skill_id, @@ -800,9 +949,11 @@ def _handle_converse_ack(self, message): context={"skill_id": self.skill_id})) # method not present in mycroft-core - def _handle_converse_request(self, message): - """Check if the targeted skill id can handle conversation - If supported, the conversation is invoked. + def _handle_converse_request(self, message: Message): + """ + If this skill is requested and supports converse, handle the user input + with `converse`. + @param message: `skill.converse.request` Message """ skill_id = message.data['skill_id'] if skill_id == self.skill_id: @@ -817,33 +968,27 @@ def _handle_converse_request(self, message): self.bus.emit(message.reply('skill.converse.response', {"skill_id": self.skill_id, "result": result})) - except Exception: + except Exception as e: + LOG.error(e) self.bus.emit(message.reply('skill.converse.response', {"skill_id": self.skill_id, "result": False})) - def converse(self, message=None): - """Handle conversation. - - This method gets a peek at utterances before the normal intent - handling process after a skill has been invoked once. - - To use, override the converse() method and return True to - indicate that the utterance has been handled. - - utterances and lang are depreciated - - Args: - message: a message object containing a message type with an - optional JSON data packet - - Returns: - bool: True if an utterance was handled, otherwise False + def converse(self, message: Optional[Message] = None) -> bool: + """ + Override to handle an utterance before intent parsing while this skill + is active. Active skills are called in order of most recently used to + least recently used until one handles the converse request. If no skill + handles an utterance in `converse`, then the utterance will continue to + normal intent parsing. + @param message: Message containing user utterances to optionally handle + @return: True if the utterance was handled, else False """ return False def __get_response(self): - """Helper to get a response from the user + """ + Helper to get a response from the user NOTE: There is a race condition here. There is a small amount of time between the end of the device speaking and the converse method @@ -857,7 +1002,7 @@ def __get_response(self): Returns: str: user's response or None on a timeout """ - + # TODO: Support `message` signature like default? def converse(utterances, lang=None): converse.response = utterances[0] if utterances else None converse.finished = True @@ -874,6 +1019,7 @@ def converse(utterances, lang=None): # AbortEvent exception to kill the thread start = time.time() while time.time() - start <= 15 and not converse.finished: + # TODO: Refactor to event-based handling time.sleep(0.1) if self.__response is not False: if self.__response is None: @@ -884,40 +1030,27 @@ def converse(utterances, lang=None): self.converse = self.__original_converse return converse.response - def get_response(self, dialog='', data=None, validator=None, - on_fail=None, num_retries=-1): - """Get response from user. - - If a dialog is supplied it is spoken, followed immediately by listening - for a user response. If the dialog is omitted listening is started - directly. - - The response can optionally be validated before returning. - - Example:: - - color = self.get_response('ask.favorite.color') - - Args: - dialog (str): Optional dialog to speak to the user - data (dict): Data used to render the dialog - validator (any): Function with following signature:: - - def validator(utterance): - return utterance != "red" - - on_fail (any): - Dialog or function returning literal string to speak on - invalid input. For example:: - - def on_fail(utterance): - return "nobody likes the color red, pick another" - - num_retries (int): Times to ask user for input, -1 for infinite - NOTE: User can not respond and timeout or say "cancel" to stop - - Returns: - str: User's reply or None if timed out or canceled + def get_response(self, dialog: str = '', data: Optional[dict] = None, + validator: Optional[Callable[[str], bool]] = None, + on_fail: Optional[Union[str, Callable[[str], str]]] = None, + num_retries: int = -1) -> Optional[str]: + """ + Get a response from the user. If a dialog is supplied it is spoken, + followed immediately by listening for a user response. If the dialog is + omitted, listening is started directly. The response may optionally be + validated before returning. + @param dialog: Optional dialog resource or string to speak + @param data: Optional data to render dialog with + @param validator: Optional method to validate user input with. Accepts + the user's utterance as an arg and returns True if it is valid. + @param on_fail: Optional string or method that accepts a failing + utterance and returns a string to be spoken when validation fails. + @param num_retries: Number of times to retry getting a user response; + -1 will retry infinitely. + * If the user asks to "cancel", this method will exit + * If the user doesn't respond and this is `-1` this will only retry + once. + @return: String user response (None if no valid response is given) """ data = data or {} @@ -949,37 +1082,42 @@ def validator_default(utterance): else: msg = dig_for_message() msg = msg.reply('mycroft.mic.listen') if msg else \ - Message('mycroft.mic.listen', context={"skill_id": self.skill_id}) + Message('mycroft.mic.listen', + context={"skill_id": self.skill_id}) self.bus.emit(msg) return self._wait_response(is_cancel, validator, on_fail_fn, num_retries) - def _wait_response(self, is_cancel, validator, on_fail, num_retries): - """Loop until a valid response is received from the user or the retry + def _wait_response(self, is_cancel: callable, validator: callable, + on_fail: callable, num_retries: int) -> Optional[str]: + """ + Loop until a valid response is received from the user or the retry limit is reached. - - Arguments: - is_cancel (callable): function checking cancel criteria - validator (callbale): function checking for a valid response - on_fail (callable): function handling retries - + @param is_cancel: Function that returns `True` if user asked to cancel + @param validator: Function that returns `True` if user input is valid + @param on_fail: Function to call if validator returns `False` + @param num_retries: Number of times to retry getting a response + @returns: User response if validated, else None """ self.__response = False self._real_wait_response(is_cancel, validator, on_fail, num_retries) while self.__response is False: + # TODO: Refactor to Event time.sleep(0.1) - return self.__response + return self.__response or None - # method not present in mycroft-core def _handle_killed_wait_response(self): + """ + Handle "stop" request when getting a response. + """ self.__response = None self.converse = self.__original_converse - # method not present in mycroft-core @killable_event("mycroft.skills.abort_question", exc=AbortQuestion, callback=_handle_killed_wait_response, react_to_stop=True) def _real_wait_response(self, is_cancel, validator, on_fail, num_retries): - """Loop until a valid response is received from the user or the retry + """ + Loop until a valid response is received from the user or the retry limit is reached. Arguments: @@ -1032,18 +1170,15 @@ def _real_wait_response(self, is_cancel, validator, on_fail, num_retries): else: self.bus.emit(msg) - def ask_yesno(self, prompt, data=None): - """Read prompt and wait for a yes/no answer - - This automatically deals with translation and common variants, - such as 'yeah', 'sure', etc. - - Args: - prompt (str): a dialog id or string to read - data (dict): response data - Returns: - string: 'yes', 'no' or whatever the user response if not - one of those, including None + def ask_yesno(self, prompt: str, + data: Optional[dict] = None) -> Optional[str]: + """ + Read prompt and wait for a yes/no answer. This automatically deals with + translation and common variants, such as 'yeah', 'sure', etc. + @param prompt: a dialog id or string to read + @param data: optional data to render dialog with + @return: 'yes', 'no' or the user response if not matched to 'yes' or + 'no', including a response of None. """ resp = self.get_response(dialog=prompt, data=data) answer = yes_or_no(resp, lang=self.lang) if resp else resp @@ -1054,9 +1189,11 @@ def ask_yesno(self, prompt, data=None): else: return resp - def ask_selection(self, options, dialog='', - data=None, min_conf=0.65, numeric=False): - """Read options, ask dialog question and wait for an answer. + def ask_selection(self, options: List[str], dialog: str = '', + data: Optional[dict] = None, min_conf: float = 0.65, + numeric: bool = False): + """ + Read options, ask dialog question and wait for an answer. This automatically deals with fuzzy matching and selection by number e.g. @@ -1108,8 +1245,15 @@ def ask_selection(self, options, dialog='', return resp # method not present in mycroft-core - def _voc_list(self, voc_filename, lang=None) -> List[str]: - + def _voc_list(self, voc_filename: str, + lang: Optional[str] = None) -> List[str]: + """ + Get list of vocab options for the requested resource and cache the + results for future references. + @param voc_filename: Name of vocab resource to get options for + @param lang: language to get vocab for (default self.lang) + @return: list of string vocab options + """ lang = lang or self.lang cache_key = lang + voc_filename @@ -1121,8 +1265,10 @@ def _voc_list(self, voc_filename, lang=None) -> List[str]: return self._voc_cache.get(cache_key) or [] - def voc_match(self, utt, voc_filename, lang=None, exact=False): - """Determine if the given utterance contains the vocabulary provided. + def voc_match(self, utt: str, voc_filename: str, lang: Optional[str] = None, + exact: bool = False): + """ + Determine if the given utterance contains the vocabulary provided. By default the method checks if the utterance contains the given vocab thereby allowing the user to say things like "yes, please" and still @@ -1158,8 +1304,9 @@ def voc_match(self, utt, voc_filename, lang=None, exact=False): return match - def report_metric(self, name, data): - """Report a skill metric to the Mycroft servers. + def report_metric(self, name: str, data: dict): + """ + Report a skill metric to the Mycroft servers. Args: name (str): Name of metric. Must use only letters and hyphens @@ -1171,8 +1318,9 @@ def report_metric(self, name, data): except Exception as e: self.log.error(f'Metric couldn\'t be uploaded, due to a network error ({e})') - def send_email(self, title, body): - """Send an email to the registered user's email. + def send_email(self, title: str, body: str): + """ + Send an email to the registered user's email. Args: title (str): Title of email @@ -1181,10 +1329,11 @@ def send_email(self, title, body): """ EmailApi().send_email(title, body, self.skill_id) - def _handle_collect_resting(self, message=None): - """Handler for collect resting screen messages. + def _handle_collect_resting(self, message: Optional[Message] = None): + """ + Handler for collect resting screen messages. - Sends info on how to trigger this skills resting page. + Sends info on how to trigger this skill's resting page. """ self.log.info('Registering resting screen') msg = message or Message("") @@ -1196,7 +1345,8 @@ def _handle_collect_resting(self, message=None): self.bus.emit(message) def register_resting_screen(self): - """Registers resting screen from the resting_screen_handler decorator. + """ + Registers resting screen from the resting_screen_handler decorator. This only allows one screen and if two is registered only one will be used. @@ -1219,12 +1369,13 @@ def register_resting_screen(self): break def _register_decorated(self): - """Register all intent handlers that are decorated with an intent. + """ + Register all intent handlers that are decorated with an intent. Looks for all functions that have been marked by a decorator and read the intent data from them. The intent handlers aren't the only decorators used. Skip properties as calling getattr on them - executes the code which may have unintended side-effects + executes the code which may have unintended side effects """ for attr_name in get_non_properties(self): method = getattr(self, attr_name) @@ -1236,8 +1387,10 @@ def _register_decorated(self): for intent_file in getattr(method, 'intent_files'): self.register_intent_file(intent_file, method) - def find_resource(self, res_name, res_dirname=None, lang=None): - """Find a resource file. + def find_resource(self, res_name: str, res_dirname: Optional[str] = None, + lang: Optional[str] = None): + """ + Find a resource file. Searches for the given filename using this scheme: 1. Search the resource lang directory: @@ -1268,8 +1421,11 @@ def find_resource(self, res_name, res_dirname=None, lang=None): f"'{lang}' not found in skill") # method not present in mycroft-core - def _on_event_start(self, message, handler_info, skill_data): - """Indicate that the skill handler is starting.""" + def _on_event_start(self, message: Message, handler_info: str, + skill_data: dict): + """ + Indicate that the skill handler is starting. + """ if handler_info: # Indicate that the skill handler is starting if requested msg_type = handler_info + '.start' @@ -1277,8 +1433,11 @@ def _on_event_start(self, message, handler_info, skill_data): self.bus.emit(message.forward(msg_type, skill_data)) # method not present in mycroft-core - def _on_event_end(self, message, handler_info, skill_data): - """Store settings and indicate that the skill handler has completed + def _on_event_end(self, message: Message, handler_info: str, + skill_data: dict): + """ + Store settings (if changed) and indicate that the skill handler has + completed. """ if self.settings != self._initial_settings: self.settings.store() @@ -1289,7 +1448,8 @@ def _on_event_end(self, message, handler_info, skill_data): self.bus.emit(message.forward(msg_type, skill_data)) # method not present in mycroft-core - def _on_event_error(self, error, message, handler_info, skill_data, speak_errors): + def _on_event_error(self, error: str, message: Message, handler_info: str, + skill_data: dict, speak_errors: bool): """Speak and log the error.""" # Convert "MyFancySkill" to "My Fancy Skill" for speaking handler_name = camel_case_split(self.name) @@ -1307,8 +1467,11 @@ def _on_event_error(self, error, message, handler_info, skill_data, speak_errors message.context["skill_id"] = self.skill_id self.bus.emit(message.forward(msg_type, skill_data)) - def add_event(self, name, handler, handler_info=None, once=False, speak_errors=True): - """Create event handler for executing intent or other event. + def add_event(self, name: str, handler: callable, + handler_info: Optional[str] = None, once: bool = False, + speak_errors: bool = True): + """ + Create event handler for executing intent or other event. Args: name (string): IntentParser name @@ -1328,7 +1491,8 @@ def on_error(error, message): self.log.info("Skill execution aborted") self._on_event_end(message, handler_info, skill_data) return - self._on_event_error(error, message, handler_info, skill_data, speak_errors) + self._on_event_error(error, message, handler_info, skill_data, + speak_errors) def on_start(message): self._on_event_start(message, handler_info, skill_data) @@ -1340,8 +1504,9 @@ def on_end(message): on_error) return self.events.add(name, wrapper, once) - def remove_event(self, name): - """Removes an event from bus emitter and events list. + def remove_event(self, name: str) -> bool: + """ + Removes an event from bus emitter and events list. Args: name (string): Name of Intent or Scheduler Event @@ -1350,8 +1515,11 @@ def remove_event(self, name): """ return self.events.remove(name) - def _register_adapt_intent(self, intent_parser, handler): - """Register an adapt intent. + def _register_adapt_intent(self, + intent_parser: Union[IntentBuilder, Intent, str], + handler: callable): + """ + Register an adapt intent. Args: intent_parser: Intent object to parse utterance for the handler. @@ -1377,8 +1545,10 @@ def _register_adapt_intent(self, intent_parser, handler): self.add_event(intent_parser.name, handler, 'mycroft.skill.handler') - def register_intent(self, intent_parser, handler): - """Register an Intent with the intent service. + def register_intent(self, intent_parser: Union[IntentBuilder, Intent, str], + handler: callable): + """ + Register an Intent with the intent service. Args: intent_parser: Intent, IntentBuilder object or padatious intent @@ -1395,7 +1565,7 @@ def register_intent(self, intent_parser, handler): return self._register_adapt_intent(intent_parser, handler) - def register_intent_file(self, intent_file, handler): + def register_intent_file(self, intent_file: str, handler: callable): """Register an Intent file with the intent service. For example: @@ -1422,7 +1592,8 @@ def register_intent_file(self, intent_file, handler): """ for lang in self._native_langs: name = f'{self.skill_id}:{intent_file}' - resource_file = ResourceFile(self._resources.types.intent, intent_file) + resource_file = ResourceFile(self._resources.types.intent, + intent_file) if resource_file.file_path is None: self.log.error(f'Unable to find "{intent_file}"') continue @@ -1431,8 +1602,9 @@ def register_intent_file(self, intent_file, handler): if handler: self.add_event(name, handler, 'mycroft.skill.handler') - def register_entity_file(self, entity_file): - """Register an Entity file with the intent service. + def register_entity_file(self, entity_file: str): + """ + Register an Entity file with the intent service. An Entity file lists the exact values that an entity can hold. For example: @@ -1457,24 +1629,29 @@ def register_entity_file(self, entity_file): name = f"{self.skill_id}:{basename(entity_file)}_{md5(entity_file.encode('utf-8')).hexdigest()}" self.intent_service.register_padatious_entity(name, filename, lang) - def handle_enable_intent(self, message): - """Listener to enable a registered intent if it belongs to this skill. + def handle_enable_intent(self, message: Message): + """ + Listener to enable a registered intent if it belongs to this skill. + @param message: `mycroft.skill.enable_intent` Message """ intent_name = message.data['intent_name'] for (name, _) in self.intent_service.detached_intents: if name == intent_name: return self.enable_intent(intent_name) - def handle_disable_intent(self, message): - """Listener to disable a registered intent if it belongs to this skill. + def handle_disable_intent(self, message: Message): + """ + Listener to disable a registered intent if it belongs to this skill. + @param message: `mycroft.skill.disable_intent` Message """ intent_name = message.data['intent_name'] for (name, _) in self.intent_service.registered_intents: if name == intent_name: return self.disable_intent(intent_name) - def disable_intent(self, intent_name): - """Disable a registered intent if it belongs to this skill. + def disable_intent(self, intent_name: str) -> bool: + """ + Disable a registered intent if it belongs to this skill. Args: intent_name (string): name of the intent to be disabled @@ -1496,8 +1673,9 @@ def disable_intent(self, intent_name): self.log.error(f'Could not disable {intent_name}, it hasn\'t been registered.') return False - def enable_intent(self, intent_name): - """(Re)Enable a registered intent if it belongs to this skill. + def enable_intent(self, intent_name: str) -> bool: + """ + (Re)Enable a registered intent if it belongs to this skill. Args: intent_name: name of the intent to be enabled @@ -1518,8 +1696,9 @@ def enable_intent(self, intent_name): self.log.error(f'Could not enable {intent_name}, it hasn\'t been registered.') return False - def set_context(self, context, word='', origin=''): - """Add context to intent service + def set_context(self, context: str, word: str = '', origin: str = ''): + """ + Add context to intent service Args: context: Keyword @@ -1534,21 +1713,37 @@ def set_context(self, context, word='', origin=''): context = self._alphanumeric_skill_id + context self.intent_service.set_adapt_context(context, word, origin) - def handle_set_cross_context(self, message): - """Add global context to intent service.""" + def remove_context(self, context: str): + """ + Remove a keyword from the context manager. + """ + if not isinstance(context, str): + raise ValueError('context should be a string') + context = self._alphanumeric_skill_id + context + self.intent_service.remove_adapt_context(context) + + def handle_set_cross_context(self, message: Message): + """ + Add global context to the intent service. + @param message: `mycroft.skill.set_cross_context` Message + """ context = message.data.get('context') word = message.data.get('word') origin = message.data.get('origin') self.set_context(context, word, origin) - def handle_remove_cross_context(self, message): - """Remove global context from intent service.""" + def handle_remove_cross_context(self, message: Message): + """ + Remove global context from the intent service. + @param message: `mycroft.skill.remove_cross_context` Message + """ context = message.data.get('context') self.remove_context(context) - def set_cross_skill_context(self, context, word=''): - """Tell all skills to add a context to intent service + def set_cross_skill_context(self, context: str, word: str = ''): + """ + Tell all skills to add a context to the intent service Args: context: Keyword @@ -1561,8 +1756,10 @@ def set_cross_skill_context(self, context, word=''): {'context': context, 'word': word, 'origin': self.skill_id})) - def remove_cross_skill_context(self, context): - """Tell all skills to remove a keyword from the context manager.""" + def remove_cross_skill_context(self, context: str): + """ + Tell all skills to remove a keyword from the context manager. + """ if not isinstance(context, str): raise ValueError('context should be a string') msg = dig_for_message() or Message("") @@ -1571,35 +1768,32 @@ def remove_cross_skill_context(self, context): self.bus.emit(msg.forward('mycroft.skill.remove_cross_context', {'context': context})) - def remove_context(self, context): - """Remove a keyword from the context manager.""" - if not isinstance(context, str): - raise ValueError('context should be a string') - context = self._alphanumeric_skill_id + context - self.intent_service.remove_adapt_context(context) - - def register_vocabulary(self, entity, entity_type, lang=None): - """ Register a word to a keyword - - Args: - entity: word to register - entity_type: Intent handler entity to tie the word to + def register_vocabulary(self, entity: str, entity_type: str, + lang: Optional[str] = None): + """ + Register a word to a keyword + @param entity: word to register + @param entity_type: Intent handler entity name to associate entity to + @param lang: language of `entity` (default self.lang) """ keyword_type = self._alphanumeric_skill_id + entity_type lang = lang or self.lang - self.intent_service.register_adapt_keyword(keyword_type, entity, lang=lang) + self.intent_service.register_adapt_keyword(keyword_type, entity, + lang=lang) - def register_regex(self, regex_str, lang=None): - """Register a new regex. - Args: - regex_str: Regex string + def register_regex(self, regex_str: str, lang: Optional[str] = None): + """ + Register a new regex. + @param regex_str: Regex string to add + @param lang: language of regex_str (default self.lang) """ self.log.debug('registering regex string: ' + regex_str) regex = munge_regex(regex_str, self.skill_id) re.compile(regex) # validate regex self.intent_service.register_adapt_regex(regex, lang=lang or self.lang) - def speak(self, utterance, expect_response=False, wait=False, meta=None): + def speak(self, utterance: str, expect_response: bool = False, + wait: bool = False, meta: Optional[dict] = None): """Speak a sentence. Args: @@ -1645,10 +1839,13 @@ def handle_output_end(msg): self.bus.on("recognizer_loop:audio_output_end", handle_output_end) event.wait(timeout=15) - self.bus.remove("recognizer_loop:audio_output_end", handle_output_end) + self.bus.remove("recognizer_loop:audio_output_end", + handle_output_end) - def speak_dialog(self, key, data=None, expect_response=False, wait=False): - """ Speak a random sentence from a dialog file. + def speak_dialog(self, key: str, data: Optional[dict] = None, + expect_response: bool = False, wait: bool = False): + """ + Speak a random sentence from a dialog file. Args: key (str): dialog file key (e.g. "hello" to speak from the file @@ -1672,8 +1869,10 @@ def speak_dialog(self, key, data=None, expect_response=False, wait=False): ) self.speak(key, expect_response, wait, {}) - def acknowledge(self): - """Acknowledge a successful request. + @staticmethod + def acknowledge(): + """ + Acknowledge a successful request. This method plays a sound to acknowledge a request that does not require a verbal response. This is intended to provide simple feedback @@ -1682,17 +1881,23 @@ def acknowledge(self): return play_acknowledge_sound() # method named init_dialog in mycroft-core - def load_dialog_files(self, root_directory=None): + def load_dialog_files(self, root_directory: Optional[str] = None): + """ + Load dialog files for all configured languages + @param root_directory: Directory to locate resources in + (default self.res_dir) + """ root_directory = root_directory or self.res_dir - # If "/dialog/" exists, load from there. Otherwise + # If "/dialog/" exists, load from there. Otherwise, # load dialog from "/locale/" for lang in self._native_langs: resources = self._load_lang(root_directory, lang) if resources.types.dialog.base_directory is None: self.log.debug(f'No dialog loaded for {lang}') - def load_data_files(self, root_directory=None): - """Called by the skill loader to load intents, dialogs, etc. + def load_data_files(self, root_directory: Optional[str] = None): + """ + Called by the skill loader to load intents, dialogs, etc. Args: root_directory (str): root folder to use when loading files. @@ -1702,7 +1907,7 @@ def load_data_files(self, root_directory=None): self.load_vocab_files(root_directory) self.load_regex_files(root_directory) - def load_vocab_files(self, root_directory=None): + def load_vocab_files(self, root_directory: Optional[str] = None): """ Load vocab files found under skill's root directory.""" root_directory = root_directory or self.res_dir for lang in self._native_langs: @@ -1743,23 +1948,28 @@ def __handle_stop(self, message): {"by": "skill:" + self.skill_id}, {"skill_id": self.skill_id})) except Exception as e: - self.log.exception(f'Failed to stop skill: {self.skill_id}') + self.log.exception(f'Failed to stop skill: {self.skill_id}: {e}') def stop(self): - """Optional method implemented by subclass.""" + """ + Optional method implemented by subclass. Called when system or user + requests `stop` to cancel current execution. + """ pass def shutdown(self): - """Optional shutdown procedure implemented by subclass. + """ + Optional shutdown procedure implemented by subclass. This method is intended to be called during the skill process - termination. The skill implementation must shutdown all processes and + termination. The skill implementation must shut down all processes and operations in execution. """ pass def default_shutdown(self): - """Parent function called internally to shut down everything. + """ + Parent function called internally to shut down everything. Shuts down known entities and calls skill specific shutdown method. """ @@ -1784,21 +1994,25 @@ def default_shutdown(self): try: self.stop() - except Exception: - self.log.error(f'Failed to stop skill: {self.skill_id}', exc_info=True) - + except Exception as e: + self.log.error(f'Failed to stop skill: {self.skill_id}: {e}', + exc_info=True) try: self.shutdown() except Exception as e: - self.log.error(f'Skill specific shutdown function encountered an error: {e}') + self.log.error(f'Skill specific shutdown function encountered an ' + f'error: {e}') self.bus.emit( Message('detach_skill', {'skill_id': str(self.skill_id) + ':'}, {"skill_id": self.skill_id})) - def schedule_event(self, handler, when, data=None, name=None, - context=None): - """Schedule a single-shot event. + def schedule_event(self, handler: callable, + when: Union[int, float, datetime.datetime], + data: Optional[dict] = None, name: Optional[str] = None, + context: Optional[dict] = None): + """ + Schedule a single-shot event. Args: handler: method to be called @@ -1820,9 +2034,14 @@ def schedule_event(self, handler, when, data=None, name=None, return self.event_scheduler.schedule_event(handler, when, data, name, context=context) - def schedule_repeating_event(self, handler, when, frequency, - data=None, name=None, context=None): - """Schedule a repeating event. + def schedule_repeating_event(self, handler: callable, + when: Union[int, float, datetime.datetime], + frequency: Union[int, float], + data: Optional[dict] = None, + name: Optional[str] = None, + context: Optional[dict] = None): + """ + Schedule a repeating event. Args: handler: method to be called @@ -1840,34 +2059,31 @@ def schedule_repeating_event(self, handler, when, frequency, message = dig_for_message() context = context or message.context if message else {} context["skill_id"] = self.skill_id - return self.event_scheduler.schedule_repeating_event( - handler, - when, - frequency, - data, - name, - context=context - ) + self.event_scheduler.schedule_repeating_event(handler, when, frequency, + data, name, + context=context) - def update_scheduled_event(self, name, data=None): - """Change data of event. + def update_scheduled_event(self, name: str, data: Optional[dict] = None): + """ + Change data of event. Args: name (str): reference name of event (from original scheduling) data (dict): event data """ - return self.event_scheduler.update_scheduled_event(name, data) + self.event_scheduler.update_scheduled_event(name, data) - def cancel_scheduled_event(self, name): - """Cancel a pending event. The event will no longer be scheduled + def cancel_scheduled_event(self, name: str): + """ + Cancel a pending event. The event will no longer be scheduled to be executed Args: name (str): reference name of event (from original scheduling) """ - return self.event_scheduler.cancel_scheduled_event(name) + self.event_scheduler.cancel_scheduled_event(name) - def get_scheduled_event_status(self, name): + def get_scheduled_event_status(self, name: str) -> int: """Get scheduled event data and return the amount of time left Args: @@ -1882,8 +2098,10 @@ def get_scheduled_event_status(self, name): return self.event_scheduler.get_scheduled_event_status(name) def cancel_all_repeating_events(self): - """Cancel any repeating events started by the skill.""" - return self.event_scheduler.cancel_all_repeating_events() + """ + Cancel any repeating events started by the skill. + """ + self.event_scheduler.cancel_all_repeating_events() class SkillGUI(GUIInterface): diff --git a/ovos_workshop/skills/decorators/__init__.py b/ovos_workshop/skills/decorators/__init__.py index dc89c06d..18588273 100644 --- a/ovos_workshop/skills/decorators/__init__.py +++ b/ovos_workshop/skills/decorators/__init__.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/converse.py b/ovos_workshop/skills/decorators/converse.py index 1c994fe9..b75b6224 100644 --- a/ovos_workshop/skills/decorators/converse.py +++ b/ovos_workshop/skills/decorators/converse.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators.converse import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/fallback_handler.py b/ovos_workshop/skills/decorators/fallback_handler.py index acfcefdc..003e994f 100644 --- a/ovos_workshop/skills/decorators/fallback_handler.py +++ b/ovos_workshop/skills/decorators/fallback_handler.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators.fallback_handler import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/killable.py b/ovos_workshop/skills/decorators/killable.py index e25241f8..d7de18d7 100644 --- a/ovos_workshop/skills/decorators/killable.py +++ b/ovos_workshop/skills/decorators/killable.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators.killable import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/layers.py b/ovos_workshop/skills/decorators/layers.py index 690a9368..2497168f 100644 --- a/ovos_workshop/skills/decorators/layers.py +++ b/ovos_workshop/skills/decorators/layers.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators.layers import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/decorators/ocp.py b/ovos_workshop/skills/decorators/ocp.py index f20850fd..32a563c5 100644 --- a/ovos_workshop/skills/decorators/ocp.py +++ b/ovos_workshop/skills/decorators/ocp.py @@ -1,2 +1,4 @@ from ovos_workshop.decorators.ocp import * # backwards compat import +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators", "0.1.0") diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 5eab2be2..87af5619 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -11,12 +11,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -"""The fallback skill implements a special type of skill handling -utterances not handled by the intent system. -""" + import operator +from typing import Optional, List, Callable, Tuple +from ovos_bus_client import MessageBusClient from ovos_utils.log import LOG from ovos_utils.messagebus import get_handler_name, Message from ovos_utils.metrics import Stopwatch @@ -40,62 +39,65 @@ class _MetaFB(OVOSSkill): class FallbackSkill(_MetaFB, metaclass=_MutableFallback): + """ + Fallbacks come into play when no skill matches an Adapt or closely with + a Padatious intent. All Fallback skills work together to give them a + view of the user's utterance. Fallback handlers are called in an order + determined the priority provided when the handler is registered. + + ======== =========================================================== + Priority Purpose + ======== =========================================================== + 0-4 High-priority fallbacks before medium-confidence Padatious + 5-89 Medium-priority fallbacks between medium and low Padatious + 90-100 Low-priority fallbacks after all other intent matches + + Handlers with the numerically lowest priority are invoked first. + Multiple fallbacks can exist at the same priority, but no order is + guaranteed. + + A Fallback can either observe or consume an utterance. A consumed + utterance will not be seen by any other Fallback handlers. + """ def __new__(cls, *args, **kwargs): if cls is FallbackSkill: - # direct instantiation of class, dynamic wizardry or unittests going on... + # direct instantiation of class, dynamic wizardry or unittests # return V2 as expected, V1 will eventually be dropped return FallbackSkillV2(*args, **kwargs) is_old = is_classic_core() if not is_old: try: - from mycroft.version import OVOS_VERSION_MAJOR, OVOS_VERSION_MINOR, OVOS_VERSION_BUILD, OVOS_VERSION_ALPHA - if OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD < 8: + from mycroft.version import OVOS_VERSION_MAJOR, \ + OVOS_VERSION_MINOR, OVOS_VERSION_BUILD, OVOS_VERSION_ALPHA + if OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and \ + OVOS_VERSION_BUILD < 8: is_old = True - elif OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and OVOS_VERSION_BUILD == 8 \ - and 0 < OVOS_VERSION_ALPHA < 5: + elif OVOS_VERSION_MAJOR == 0 and OVOS_VERSION_MINOR == 0 and \ + OVOS_VERSION_BUILD == 8 and 0 < OVOS_VERSION_ALPHA < 5: is_old = True except ImportError: pass if is_old: + LOG.debug("Using V1 Fallback") cls.__bases__ = (FallbackSkillV1, FallbackSkill, _MetaFB) else: + LOG.debug("Using V2 Fallback") cls.__bases__ = (FallbackSkillV2, FallbackSkill, _MetaFB) return super().__new__(cls, *args, **kwargs) @classmethod - def make_intent_failure_handler(cls, bus): - """backwards compat, old version of ovos-core call this method to bind the bus to old class""" + def make_intent_failure_handler(cls, bus: MessageBusClient): + """ + backwards compat, old version of ovos-core call this method to bind + the bus to old class + """ return FallbackSkillV1.make_intent_failure_handler(bus) - class FallbackSkillV1(_MetaFB, metaclass=_MutableFallback): - """Fallbacks come into play when no skill matches an Adapt or closely with - a Padatious intent. All Fallback skills work together to give them a - view of the user's utterance. Fallback handlers are called in an order - determined the priority provided when the the handler is registered. - - ======== ======== ================================================ - Priority Who? Purpose - ======== ======== ================================================ - 1-4 RESERVED Unused for now, slot for pre-Padatious if needed - 5 MYCROFT Padatious near match (conf > 0.8) - 6-88 USER General - 89 MYCROFT Padatious loose match (conf > 0.5) - 90-99 USER Uncaught intents - 100+ MYCROFT Fallback Unknown or other future use - ======== ======== ================================================ - - Handlers with the numerically lowest priority are invoked first. - Multiple fallbacks can exist at the same priority, but no order is - guaranteed. - - A Fallback can either observe or consume an utterance. A consumed - utterance will not be see by any other Fallback handlers. - """ fallback_handlers = {} - wrapper_map = [] # Map containing (handler, wrapper) tuples + wrapper_map: List[Tuple[callable, callable]] = [] # [(handler, wrapper)] def __init__(self, name=None, bus=None, use_settings=True): super().__init__(name, bus, use_settings) @@ -106,8 +108,10 @@ def __init__(self, name=None, bus=None, use_settings=True): self.fallback_config = self.config_core["skills"].get("fallbacks", {}) @classmethod - def make_intent_failure_handler(cls, bus): - """Goes through all fallback handlers until one returns True""" + def make_intent_failure_handler(cls, bus: MessageBusClient): + """ + Goes through all fallback handlers until one returns True + """ def handler(message): # No hard limit to 100, while not officially supported @@ -159,15 +163,16 @@ def handler(message): return handler @staticmethod - def _report_timing(ident, system, timing, additional_data=None): - """Create standardized message for reporting timing. - - Args: - ident (str): identifier of user interaction - system (str): system the that's generated the report - timing (stopwatch): Stopwatch object with recorded timing - additional_data (dict): dictionary with related data + def _report_timing(ident: str, system: str, timing: Stopwatch, + additional_data: Optional[dict] = None): + """ + Create standardized message for reporting timing. + @param ident: identifier for user interaction + @param system: identifier for system being timed + @param timing: Stopwatch object with recorded timing + @param additional_data: Optional dict data to include with metric """ + # TODO: Move to an imported function and deprecate this try: from mycroft.metrics import report_timing report_timing(ident, system, timing, additional_data) @@ -175,19 +180,13 @@ def _report_timing(ident, system, timing, additional_data=None): pass @classmethod - def _register_fallback(cls, handler, wrapper, priority): - """Register a function to be called as a general info fallback - Fallback should receive message and return - a boolean (True if succeeded or False if failed) - - Lower priority gets run first - 0 for high priority 100 for low priority - - Args: - handler (callable): original handler, used as a reference when - removing - wrapper (callable): wrapped version of handler - priority (int): fallback priority + def _register_fallback(cls, handler: callable, wrapper: callable, + priority: int): + """ + Add a fallback handler to the class + @param handler: original handler method used for reference + @param wrapper: wrapped handler used to handle fallback requests + @param priority: fallback priority """ while priority in cls.fallback_handlers: priority += 1 @@ -195,9 +194,14 @@ def _register_fallback(cls, handler, wrapper, priority): cls.fallback_handlers[priority] = wrapper cls.wrapper_map.append((handler, wrapper)) - def register_fallback(self, handler, priority): - """Register a fallback with the list of fallback handlers and with the - list of handlers registered by this instance + def register_fallback(self, handler: Callable[[Message], None], + priority: int): + """ + Register a fallback handler method with a given priority. This will + account for configuration overrides of fallback priority, as well as + configured fallback skill whitelist/blacklist. + @param handler: fallback handler method that accepts a `Message` arg + @param priority: fallback priority """ opmode = self.fallback_config.get("fallback_mode", FallbackMode.ACCEPT_ALL) @@ -225,14 +229,11 @@ def wrapper(*args, **kwargs): self._register_fallback(handler, wrapper, priority) @classmethod - def _remove_registered_handler(cls, wrapper_to_del): - """Remove a registered wrapper. - - Args: - wrapper_to_del (callable): wrapped handler to be removed - - Returns: - (bool) True if one or more handlers were removed, otherwise False. + def _remove_registered_handler(cls, wrapper_to_del: callable) -> bool: + """ + Remove a registered wrapper. + @param wrapper_to_del: wrapped handler to be removed + @return: True if one or more handlers were removed, otherwise False. """ found_handler = False for priority, handler in list(cls.fallback_handlers.items()): @@ -245,23 +246,22 @@ def _remove_registered_handler(cls, wrapper_to_del): return found_handler @classmethod - def remove_fallback(cls, handler_to_del): - """Remove a fallback handler. - - Args: - handler_to_del: reference to handler - Returns: - (bool) True if at least one handler was removed, otherwise False + def remove_fallback(cls, handler_to_del: callable) -> bool: + """ + Remove a fallback handler. + @param handler_to_del: registered callback handler (or wrapped handler) + @return: True if at least one handler was removed, otherwise False """ # Find wrapper from handler or wrapper wrapper_to_del = None for h, w in cls.wrapper_map: if handler_to_del in (h, w): + handler_to_del = h wrapper_to_del = w break if wrapper_to_del: - cls.wrapper_map.remove((h, w)) + cls.wrapper_map.remove((handler_to_del, wrapper_to_del)) remove_ok = cls._remove_registered_handler(wrapper_to_del) else: LOG.warning('Could not find matching fallback handler') @@ -269,24 +269,29 @@ def remove_fallback(cls, handler_to_del): return remove_ok def remove_instance_handlers(self): - """Remove all fallback handlers registered by the fallback skill.""" - self.log.info('Removing all handlers...') + """ + Remove all fallback handlers registered by the fallback skill. + """ + LOG.info('Removing all handlers...') while len(self.instance_fallback_handlers): handler = self.instance_fallback_handlers.pop() self.remove_fallback(handler) def default_shutdown(self): - """Remove all registered handlers and perform skill shutdown.""" + """ + Remove all registered handlers and perform skill shutdown. + """ self.remove_instance_handlers() super().default_shutdown() def _register_decorated(self): - """Register all intent handlers that are decorated with an intent. + """ + Register all decorated fallback handlers. Looks for all functions that have been marked by a decorator - and read the intent data from them. The intent handlers aren't the - only decorators used. Skip properties as calling getattr on them - executes the code which may have unintended side-effects + and read the fallback priority from them. The handlers aren't the + only decorators used. Skip properties as calling getattr on them + executes the code which may have unintended side effects. """ super()._register_decorated() for attr_name in get_non_properties(self): @@ -296,44 +301,15 @@ def _register_decorated(self): class FallbackSkillV2(_MetaFB, metaclass=_MutableFallback): - """ - Fallbacks come into play when no skill matches an intent. - - Fallback handlers are called in an order determined the - priority provided when the skill is registered. - - ======== ======== ================================================ - Priority Who? Purpose - ======== ======== ================================================ - 1-4 RESERVED Unused for now, slot for pre-Padatious if needed - 5 MYCROFT Padatious near match (conf > 0.8) - 6-88 USER General - 89 MYCROFT Padatious loose match (conf > 0.5) - 90-99 USER Uncaught intents - 100+ MYCROFT Fallback Unknown or other future use - ======== ======== ================================================ - - Handlers with the numerically lowest priority are invoked first. - Multiple fallbacks can exist at the same priority, but no order is - guaranteed. - - A Fallback can either observe or consume an utterance. A consumed - utterance will not be see by any other Fallback handlers. - - A skill might register several handlers, the lowest priority will be reported to core - If a skill is selected by core then all handlers are checked by - their priority until one can handle the utterance - - A skill may return False in the can_answer method to request - that core does not execute it's fallback handlers - """ - # "skill_id": priority (int) overrides fallback_config = Configuration().get("skills", {}).get("fallbacks", {}) @classmethod - def make_intent_failure_handler(cls, bus): - """backwards compat, old version of ovos-core call this method to bind the bus to old class""" + def make_intent_failure_handler(cls, bus: MessageBusClient): + """ + backwards compat, old version of ovos-core call this method to bind + the bus to old class + """ return FallbackSkillV1.make_intent_failure_handler(bus) def __init__(self, bus=None, skill_id=""): @@ -341,7 +317,13 @@ def __init__(self, bus=None, skill_id=""): super().__init__(bus=bus, skill_id=skill_id) @property - def priority(self): + def priority(self) -> int: + """ + Get this skill's minimum priority. Priority is determined as: + 1) Configured fallback skill priority + 2) Highest fallback handler priority + 3) Default `101` (no fallback handlers are registered) + """ priority_overrides = self.fallback_config.get("fallback_priorities", {}) if self.skill_id in priority_overrides: return priority_overrides.get(self.skill_id) @@ -349,30 +331,35 @@ def priority(self): return min([p[0] for p in self._fallback_handlers]) return 101 - def can_answer(self, utterances, lang): - """Check if the skill can answer the particular question. - - - Arguments: - utterances (list): list of possible transcriptions to parse - lang (str) : lang code - Returns: - (bool) True if skill can handle the query + def can_answer(self, utterances: List[str], lang: str) -> bool: + """ + Check if the skill can answer the particular question. Override this + method to validate whether a query can possibly be handled. By default, + assumes a skill can answer if it has any registered handlers + @param utterances: list of possible transcriptions to parse + @param lang: BCP-47 language code associated with utterances + @return: True if skill can handle the query """ return len(self._fallback_handlers) > 0 def _register_system_event_handlers(self): - """Add all events allowing the standard interaction with the Mycroft - system. + """ + Register messagebus event handlers and emit a message to register this + fallback skill. """ super()._register_system_event_handlers() - self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, speak_errors=False) - self.add_event(f"ovos.skills.fallback.{self.skill_id}.request", self._handle_fallback_request, speak_errors=False) + self.add_event('ovos.skills.fallback.ping', self._handle_fallback_ack, + speak_errors=False) + self.add_event(f"ovos.skills.fallback.{self.skill_id}.request", + self._handle_fallback_request, speak_errors=False) self.bus.emit(Message("ovos.skills.fallback.register", - {"skill_id": self.skill_id, "priority": self.priority})) + {"skill_id": self.skill_id, + "priority": self.priority})) - def _handle_fallback_ack(self, message): - """Inform skills service we can handle fallbacks.""" + def _handle_fallback_ack(self, message: Message): + """ + Inform skills service we can handle fallbacks. + """ utts = message.data.get("utterances", []) lang = message.data.get("lang") self.bus.emit(message.reply( @@ -381,14 +368,22 @@ def _handle_fallback_ack(self, message): "can_handle": self.can_answer(utts, lang)}, context={"skill_id": self.skill_id})) - def _handle_fallback_request(self, message): + def _handle_fallback_request(self, message: Message): + """ + Handle a fallback request, calling any registered handlers in priority + order until one is successful. emits a response indicating whether the + request was handled. + @param message: `ovos.skills.fallback..request` message + """ # indicate fallback handling start - self.bus.emit(message.forward(f"ovos.skills.fallback.{self.skill_id}.start")) + self.bus.emit(message.forward( + f"ovos.skills.fallback.{self.skill_id}.start")) handler_name = None # each skill can register multiple handlers with different priorities - sorted_handlers = sorted(self._fallback_handlers, key=operator.itemgetter(0)) + sorted_handlers = sorted(self._fallback_handlers, + key=operator.itemgetter(0)) for prio, handler in sorted_handlers: try: if handler(message): @@ -401,16 +396,20 @@ def _handle_fallback_request(self, message): else: status = False - self.bus.emit(message.forward(f"ovos.skills.fallback.{self.skill_id}.response", - data={"result": status, - "fallback_handler": handler_name})) + self.bus.emit(message.forward( + f"ovos.skills.fallback.{self.skill_id}.response", + data={"result": status, "fallback_handler": handler_name})) - def register_fallback(self, handler, priority): - """Register a fallback with the list of fallback handlers and with the - list of handlers registered by this instance + def register_fallback(self, handler: callable, priority: int): + """ + Register a fallback handler and add a messagebus handler to call it on + any fallback request. + @param handler: Fallback handler + @param priority: priority of the registered handler """ - LOG.info(f"registering fallback handler -> ovos.skills.fallback.{self.skill_id}") + LOG.info(f"registering fallback handler -> " + f"ovos.skills.fallback.{self.skill_id}") def wrapper(*args, **kwargs): if handler(*args, **kwargs): @@ -422,18 +421,22 @@ def wrapper(*args, **kwargs): self.bus.on(f"ovos.skills.fallback.{self.skill_id}", wrapper) def default_shutdown(self): - """Remove all registered handlers and perform skill shutdown.""" - self.bus.emit(Message("ovos.skills.fallback.deregister", {"skill_id": self.skill_id})) + """ + Remove all registered handlers and perform skill shutdown. + """ + self.bus.emit(Message("ovos.skills.fallback.deregister", + {"skill_id": self.skill_id})) self.bus.remove_all_listeners(f"ovos.skills.fallback.{self.skill_id}") super().default_shutdown() def _register_decorated(self): - """Register all intent handlers that are decorated with an intent. + """ + Register all decorated fallback handlers. Looks for all functions that have been marked by a decorator - and read the intent data from them. The intent handlers aren't the - only decorators used. Skip properties as calling getattr on them - executes the code which may have unintended side-effects + and read the fallback priority from them. The handlers aren't the + only decorators used. Skip properties as calling getattr on them + executes the code which may have unintended side effects. """ super()._register_decorated() for attr_name in get_non_properties(self): diff --git a/ovos_workshop/skills/idle_display_skill.py b/ovos_workshop/skills/idle_display_skill.py index d239659c..7f680482 100644 --- a/ovos_workshop/skills/idle_display_skill.py +++ b/ovos_workshop/skills/idle_display_skill.py @@ -11,68 +11,76 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Provide a common API for skills that define idle screen functionality. - -The idle display should show when no other skill is using the display. Some -skills use the display for a defined period of time before returning to the -idle display (e.g. Weather Skill). Some skills take control of the display -indefinitely (e.g. Timer Skill). - -The display could be a touch screen (such as on the Mark II), or an -Arduino LED array (such as on the Mark I), or any other type of display. This -base class is meant to be agnostic to the type of display, with the -implementation details defined within the skill that uses this as a base class. -""" -from ovos_utils.log import LOG + +from ovos_utils.log import LOG, log_deprecation from ovos_utils.messagebus import Message -from ovos_workshop.skills.mycroft_skill import MycroftSkill +from ovos_workshop.skills.base import BaseSkill -class IdleDisplaySkill(MycroftSkill): - """Base class for skills that define an idle display. +class IdleDisplaySkill(BaseSkill): + """ + Base class for skills that define an idle display. An idle display is what shows on a device's screen when it is not in use - by other skills. For example, Mycroft's Home Screen Skill. + by other skills. i.e. a Home Screen skill. + + The idle display should show when no other skill is using the display. Some + skills use the display for a defined period of time before returning to the + idle display (e.g. Weather Skill). Some skills take control of the display + indefinitely (e.g. Timer Skill). + + The display could be a touch screen (such as on the Mark II), or an + Arduino LED array (such as on the Mark I), or any other type of display. + This base class is meant to be agnostic to the type of display. """ def __init__(self, *args, **kwargs): - super(IdleDisplaySkill, self).__init__(*args, **kwargs) + BaseSkill.__init__(self, *args, **kwargs) self._homescreen_entry = None - def bind(self, bus): - """Tasks to complete during skill load but after bus initialization.""" - if bus: - super().bind(bus) - self._define_message_bus_handlers() - self._build_homescreen_entry() - def handle_idle(self): - """Override this method to display the idle screen.""" + """ + Override this method to display the idle screen. + """ raise NotImplementedError( - "Subclass must override the handle_idle method" - ) + "Subclass must override the handle_idle method") - def _define_message_bus_handlers(self): - """Defines the bus events handled in this skill and their handlers.""" + def _register_system_event_handlers(self): + """ + Defines the bus events handled in this skill and their handlers. + """ + BaseSkill._register_system_event_handlers(self) self.add_event("mycroft.ready", self._handle_mycroft_ready) - self.add_event("homescreen.manager.activate.display", self._display_homescreen_requested) - self.add_event("homescreen.manager.reload.list", self._reload_homescreen_entry) - self.add_event("mycroft.skills.shutdown", self._remove_homescreen_on_shutdown) + self.add_event("homescreen.manager.activate.display", + self._display_homescreen_requested) + self.add_event("homescreen.manager.reload.list", + self._reload_homescreen_entry) + self.add_event("mycroft.skills.shutdown", + self._remove_homescreen_on_shutdown) + self._build_homescreen_entry() def _handle_mycroft_ready(self, message): - """Shows idle screen when device is ready for use.""" + """ + Shows idle screen when device is ready for use. + """ self._show_idle_screen() self.bus.emit(message.reply("skill.idle.displayed")) LOG.debug("Homescreen ready") def _show_idle_screen(self): - """Method for compat with mycroft-core mark2/qa branch equivalent class - Skills made for mk2 will override this private method instead of the public handle_idle """ + Backwards-compat method for pre-Dinkum Mycroft Mark2 skills + """ + log_deprecation("Call `self.handle_idle()` directly", "0.1.0") self.handle_idle() - def _build_homescreen_entry(self, message=None): + def _build_homescreen_entry(self, message: Message = None): + """ + Update the internal _homescreen_entry object + for this skill and send it to the Home Screen Manager. + @param message: optional Message associated with request + """ # get the super class this inherits from super_class_name = "IdleDisplaySkill" super_class_object = self.__class__.__name__ @@ -81,27 +89,46 @@ def _build_homescreen_entry(self, message=None): "id": self.skill_id} self._add_available_homescreen(message) - def _add_available_homescreen(self, message=None): + def _add_available_homescreen(self, message: Message = None): + """ + Add this skill's homescreen_entry to the Home Screen Manager. + @param message: optional Message associated with request + """ message = message or Message("homescreen.manager.reload.list") LOG.debug(f"Registering Homescreen {self._homescreen_entry}") msg = message.forward("homescreen.manager.add", self._homescreen_entry) self.bus.emit(msg) - def _remove_homescreen(self, message): + def _remove_homescreen(self, message: Message): + """ + Remove this skill's homescreen_entry from the Home Screen Manager + @param message: `mycroft.skills.shutdown` message + """ LOG.debug(f"Requesting removal of {self._homescreen_entry}") msg = message.forward("homescreen.manager.remove", self._homescreen_entry) self.bus.emit(msg) - def _reload_homescreen_entry(self, message): + def _reload_homescreen_entry(self, message: Message): + """ + Reload this skill's homescreen_entry and send it to the + Home Screen Manager. + @param message: `homescreen.manager.reload.list` message + """ self._build_homescreen_entry(message) - def _remove_homescreen_on_shutdown(self, message): - shutdown_for_id = message.data["id"] - if shutdown_for_id == self.skill_id: + def _remove_homescreen_on_shutdown(self, message: Message): + """ + Remove this homescreen from the Home Screen Manager if requested + @param message: `mycroft.skills.shutdown` message + """ + if message.data["id"] == self.skill_id: self._remove_homescreen(message) - def _display_homescreen_requested(self, message): - request_for_id = message.data["homescreen_id"] - if request_for_id == self.skill_id: + def _display_homescreen_requested(self, message: Message): + """ + Display this home screen if requested by the Home Screen Manager + @param message: `homescreen.manager.activate.display` message + """ + if message.data["homescreen_id"] == self.skill_id: self._show_idle_screen() self.bus.emit(message.reply("skill.idle.displayed")) diff --git a/ovos_workshop/skills/intent_provider.py b/ovos_workshop/skills/intent_provider.py index 5a230669..f85a389e 100644 --- a/ovos_workshop/skills/intent_provider.py +++ b/ovos_workshop/skills/intent_provider.py @@ -1,14 +1,14 @@ from threading import Event from time import time as get_time, sleep -from ovos_utils.log import LOG +from ovos_utils.log import LOG, log_deprecation from ovos_utils.messagebus import Message -from ovos_workshop.skills.ovos import OVOSFallbackSkill +from ovos_workshop.skills.fallback import FallbackSkill from ovos_config.config import read_mycroft_config, update_mycroft_config class BaseIntentEngine: - # TODO move to OPM def __init__(self, name, config=None): + log_deprecation("This base class is not supported", "0.1.0") self.name = name.lower() config = config or read_mycroft_config() self.config = config.get(self.name, {}) @@ -49,8 +49,9 @@ def calc_intent(self, query): return data -class IntentEngineSkill(OVOSFallbackSkill): +class IntentEngineSkill(FallbackSkill): def __init__(self, *args, **kwargs): + log_deprecation("This base class is not supported", "0.1.0") super().__init__(*args, **kwargs) self.engine = None self.config = {} diff --git a/ovos_workshop/skills/layers.py b/ovos_workshop/skills/layers.py index 32a9f766..dcedc619 100644 --- a/ovos_workshop/skills/layers.py +++ b/ovos_workshop/skills/layers.py @@ -1 +1,3 @@ from ovos_workshop.decorators.layers import IntentLayers +from ovos_utils.log import log_deprecation +log_deprecation("Import from `ovos_workshop.decorators.layers`", "0.1.0") diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py index 92d5dd23..4ea70744 100644 --- a/ovos_workshop/skills/mycroft_skill.py +++ b/ovos_workshop/skills/mycroft_skill.py @@ -11,26 +11,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# -"""Common functionality relating to the implementation of mycroft skills.""" -import inspect import shutil -from abc import ABCMeta -from os.path import join, exists, dirname -from ovos_utils.log import LOG +from abc import ABCMeta +from os.path import join, exists +from typing import Optional +from ovos_bus_client import MessageBusClient, Message +from ovos_utils.log import LOG, log_deprecation from ovos_workshop.skills.base import BaseSkill, is_classic_core class _SkillMetaclass(ABCMeta): """ - this metaclass ensures we can load skills like regular python objects - mycroft-core required a skill loader helper class, which created the skill and then finished object init - this means skill_id and bus are not available in init method, mycroft introduced a method named initialize meant for this + This metaclass ensures we can load skills like regular python objects. + mycroft-core required a skill loader helper class, which created the skill + and then finished object init. This meant skill_id and bus were not + available in init method, so mycroft introduced a method named `initialize` + that was called after `skill_id` and `bus` were defined. - to make skills pythonic and standalone, this metaclass is used to auto init old skills and help in migrating to new standards + To make skills pythonic and standalone, this metaclass is used to auto init + old skills and help in migrating to new standards. To override isinstance checks we also need to use a metaclass @@ -47,11 +49,13 @@ def __call__(cls, *args, **kwargs): for a in args: if isinstance(a, MessageBusClient) or isinstance(a, FakeBus): bus = a - LOG.warning(f"bus should be a kwarg, guessing {a} is the bus") + LOG.warning( + f"bus should be a kwarg, guessing {a} is the bus") break else: - LOG.warning("skill initialized without bus!! this is legacy behaviour and" - " requires you to call skill.bind(bus) or skill._startup(skill_id, bus)\n" + LOG.warning("skill initialized without bus!! this is legacy " + "behaviour and requires you to call skill.bind(bus)" + " or skill._startup(skill_id, bus)\n" "bus will be required starting on ovos-core 0.1.0") return super().__call__(*args, **kwargs) @@ -60,37 +64,45 @@ def __call__(cls, *args, **kwargs): if "bus" in kwargs: bus = kwargs.pop("bus") if not skill_id: - LOG.warning(f"skill_id should be a kwarg, please update {cls.__name__}") + LOG.warning(f"skill_id should be a kwarg, please update " + f"{cls.__name__}") if args and isinstance(args[0], str): a = args[0] - if a[0].isupper(): # in mycroft name is CamelCase by convention, not skill_id - LOG.debug(f"ambiguous skill_id, ignoring {a} as it appears to be a CamelCase name") + if a[0].isupper(): + # in mycroft name is CamelCase by convention, not skill_id + LOG.debug(f"ambiguous skill_id, ignoring {a} as it appears " + f"to be a CamelCase name") else: - LOG.warning(f"ambiguous skill_id, assuming positional argument: {a}") + LOG.warning(f"ambiguous skill_id, assuming positional " + f"argument: {a}") skill_id = a if not skill_id: - LOG.warning("skill initialized without skill_id!! this is legacy behaviour and" - " requires you to call skill._startup(skill_id, bus)\n" - "skill_id will be required starting on ovos-core 0.1.0") + LOG.warning("skill initialized without bus!! this is legacy " + "behaviour and requires you to call skill.bind(bus)" + " or skill._startup(skill_id, bus)\n" + "bus will be required starting on ovos-core 0.1.0") return super().__call__(*args, **kwargs) # by convention skill_id is the folder name # usually repo.author # TODO - uncomment once above is deprecated - #skill_id = dirname(inspect.getfile(cls)).split("/")[-1] - #LOG.warning(f"missing skill_id, assuming folder name convention: {skill_id}") + # skill_id = dirname(inspect.getfile(cls)).split("/")[-1] + # LOG.warning(f"missing skill_id, assuming folder name " + # f"convention: {skill_id}") try: - # skill follows latest best practices, accepts kwargs and does its own init + # skill follows latest best practices, + # accepts kwargs and does its own init return super().__call__(skill_id=skill_id, bus=bus, **kwargs) except TypeError: - LOG.warning("legacy skill signature detected, attempting to init skill manually, " - f"self.bus and self.skill_id will only be available in self.initialize.\n" + - f"__init__ method needs to accept `skill_id` and `bus` to resolve this.") + LOG.warning("legacy skill signature detected, attempting to init " + "skill manually, self.bus and self.skill_id will only " + "be available in self.initialize.\n__init__ method " + "needs to accept `skill_id` and `bus` to resolve this.") - # skill did not update its init method, let's do some magic to init it manually - # NOTE: no try: except because all skills must accept this initialization and we want exception + # skill did not update its init method, init it manually + # NOTE: no try/except because all skills must accept this initialization # this is what skill loader does internally skill = super().__call__(*args, **kwargs) skill._startup(bus, skill_id) @@ -107,22 +119,20 @@ def __instancecheck__(self, instance): class MycroftSkill(BaseSkill, metaclass=_SkillMetaclass): - """Base class for mycroft skills providing common behaviour and parameters - to all Skill implementations. - - For information on how to get started with creating mycroft skills see - https://mycroft.ai/documentation/skills/introduction-developing-skills/ - - New methods added here are always private, public apis for Skill class are added in OVOSSkill - This is done to ensure no syntax errors when a MycroftSkill object comes from mycroft-core - - Args: - name (str): skill name - bus (MycroftWebsocketClient): Optional bus connection - use_settings (bool): Set to false to not use skill settings at all (DEPRECATED) + """ + Base class for mycroft skills providing common behaviour and parameters + to all Skill implementations. This class is kept for backwards-compat. It is + recommended to implement `OVOSSkill` to properly implement new methods. """ - def __init__(self, name=None, bus=None, use_settings=True, *args, **kwargs): + def __init__(self, name: str = None, bus: MessageBusClient = None, + use_settings: bool = True, *args, **kwargs): + """ + Create a MycroftSkill object. + @param name: DEPRECATED skill_name + @param bus: MessageBusClient to bind to skill + @param use_settings: DEPRECATED option to disable settings sync + """ super().__init__(name=name, bus=bus, *args, **kwargs) self._initial_settings = {} @@ -131,20 +141,24 @@ def __init__(self, name=None, bus=None, use_settings=True, *args, **kwargs): # old kludge from fallback skills, unused according to grep if use_settings is False: - LOG.warning("use_settings has been deprecated! skill settings are always enabled") + log_deprecation("use_settings has been deprecated! " + "skill settings are always enabled", "0.1.0") if is_classic_core(): self.settings_write_path = self.root_dir def _init_settings_manager(self): super()._init_settings_manager() - # backwards compat - self.settings_meta has been deprecated in favor of settings manager + # backwards compat - self.settings_meta has been deprecated + # in favor of settings manager if is_classic_core(): from mycroft.skills.settings import SettingsMetaUploader else: try: # ovos-core compat layer - from mycroft.deprecated.skills.settings import SettingsMetaUploader - self._settings_meta = SettingsMetaUploader(self.root_dir, self.skill_id) + from mycroft.deprecated.skills.settings import \ + SettingsMetaUploader + self._settings_meta = SettingsMetaUploader(self.root_dir, + self.skill_id) except ImportError: pass # standalone skill, skip backwards compat property @@ -152,32 +166,42 @@ def _init_settings(self): """Setup skill settings.""" if is_classic_core(): # migrate settings if needed - if not exists(self._settings_path) and exists(self._old_settings_path): - LOG.warning("Found skill settings at pre-xdg location, migrating!") + if not exists(self._settings_path) and \ + exists(self._old_settings_path): + LOG.warning("Found skill settings at pre-xdg location, " + "migrating!") shutil.copy(self._old_settings_path, self._settings_path) - LOG.info(f"{self._old_settings_path} moved to {self._settings_path}") + LOG.info(f"{self._old_settings_path} moved to " + f"{self._settings_path}") super()._init_settings() # renamed in base class for naming consistency - def init_dialog(self, root_directory=None): - """ DEPRECATED: use load_dialog_files instead """ + def init_dialog(self, root_directory: Optional[str] = None): + """ + DEPRECATED: use load_dialog_files instead + """ + log_deprecation("Use `load_dialog_files`", "0.1.0") self.load_dialog_files(root_directory) # renamed in base class for naming consistency def make_active(self): - """Bump skill to active_skill list in intent_service. + """ + Bump skill to active_skill list in intent_service. This enables converse method to be called even without skill being used in last 5 minutes. - deprecated: use self.activate() instead + deprecated: use self._activate() instead """ + log_deprecation("Use `_activate`", "0.1.0") self._activate() # patched due to functional (internal) differences under mycroft-core - def _on_event_end(self, message, handler_info, skill_data): - """Store settings and indicate that the skill handler has completed + def _on_event_end(self, message: Message, handler_info: str, + skill_data: dict): + """ + Store settings and indicate that the skill handler has completed """ if not is_classic_core(): return super()._on_event_end(message, handler_info, skill_data) @@ -189,7 +213,7 @@ def _on_event_end(self, message, handler_info, skill_data): save_settings(self.settings_write_path, self.settings) self._initial_settings = dict(self.settings) except Exception as e: - LOG.exception("Failed to save skill settings") + LOG.exception(f"Failed to save skill settings: {e}") if handler_info: msg_type = handler_info + '.complete' message.context["skill_id"] = self.skill_id @@ -197,47 +221,61 @@ def _on_event_end(self, message, handler_info, skill_data): # renamed in base class for naming consistency # refactored to use new resource utils - def translate(self, text, data=None): - """Deprecated method for translating a dialog file. - use self._resources.render_dialog(text, data) instead""" + def translate(self, text: str, data: Optional[dict] = None): + """ + Deprecated method for translating a dialog file. + use self._resources.render_dialog(text, data) instead + """ + log_deprecation("Use `_resources.render_dialog`", "0.1.0") return self._resources.render_dialog(text, data) # renamed in base class for naming consistency # refactored to use new resource utils - def translate_namedvalues(self, name, delim=','): - """Deprecated method for translating a name/value file. - use elf._resources.load_named_value_filetext, data) instead""" + def translate_namedvalues(self, name: str, delim: str = ','): + """ + Deprecated method for translating a name/value file. + use self._resources.load_named_value_filetext, data) instead + """ + log_deprecation("Use `_resources.load_named_value_file`", "0.1.0") return self._resources.load_named_value_file(name, delim) # renamed in base class for naming consistency # refactored to use new resource utils - def translate_list(self, list_name, data=None): - """Deprecated method for translating a list. - use delf._resources.load_list_file(text, data) instead""" + def translate_list(self, list_name: str, data: Optional[dict] = None): + """ + Deprecated method for translating a list. + use delf._resources.load_list_file(text, data) instead + """ + log_deprecation("Use `_resources.load_list_file`", "0.1.0") return self._resources.load_list_file(list_name, data) # renamed in base class for naming consistency # refactored to use new resource utils - def translate_template(self, template_name, data=None): - """Deprecated method for translating a template file - use delf._resources.template_file(text, data) instead""" + def translate_template(self, template_name: str, + data: Optional[dict] = None): + """ + Deprecated method for translating a template file + use delf._resources.template_file(text, data) instead + """ + log_deprecation("Use `_resources.template_file`", "0.1.0") return self._resources.load_template_file(template_name, data) # refactored - backwards compat + log warnings @property def settings_meta(self): - LOG.warning("self.settings_meta has been deprecated! please use self.settings_manager instead") + log_deprecation("Use `self.settings_manager`", "0.1.0") return self._settings_meta # refactored - backwards compat + log warnings @settings_meta.setter def settings_meta(self, val): - LOG.warning("self.settings_meta has been deprecated! please use self.settings_manager instead") + log_deprecation("Use `self.settings_manager`", "0.1.0") self._settings_meta = val # internal - deprecated under ovos-core @property def _old_settings_path(self): + log_deprecation("This path is no longer used", "0.1.0") old_dir = self.config_core.get("data_dir") or "/opt/mycroft" old_folder = self.config_core.get("skills", {}).get("msm", {}) \ .get("directory") or "skills" @@ -247,8 +285,9 @@ def _old_settings_path(self): @property def _settings_path(self): if is_classic_core(): - if self.settings_write_path and self.settings_write_path != self.root_dir: - LOG.warning("self.settings_write_path has been deprecated! " - "Support will be dropped in a future release") + if self.settings_write_path and \ + self.settings_write_path != self.root_dir: + log_deprecation("`self.settings_write_path` is no longer used", + "0.1.0") return join(self.settings_write_path, 'settings.json') return super()._settings_path diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 8cee649e..65707178 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -1,17 +1,18 @@ import re -import time -from typing import List +from threading import Event +from typing import List, Optional, Union + +from ovos_bus_client import MessageBusClient +from ovos_bus_client.message import Message, dig_for_message from ovos_utils.intents import IntentBuilder, Intent -from ovos_utils.log import LOG -from ovos_utils.messagebus import Message, dig_for_message +from ovos_utils.log import LOG, log_deprecation from ovos_utils.skills import get_non_properties from ovos_utils.skills.audioservice import OCPInterface from ovos_utils.skills.settings import PrivateSettings from ovos_utils.sound import play_audio +from ovos_workshop.resource_files import SkillResources -from ovos_workshop.decorators.killable import killable_event, \ - AbortQuestion from ovos_workshop.skills.layers import IntentLayers from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core @@ -34,7 +35,7 @@ def __init__(self, *args, **kwargs): self.audio_service = None super(OVOSSkill, self).__init__(*args, **kwargs) - def bind(self, bus): + def bind(self, bus: MessageBusClient): super().bind(bus) if bus: # here to ensure self.skill_id is populated @@ -44,117 +45,153 @@ def bind(self, bus): # new public api, these are not available in MycroftSkill @property - def is_fully_initialized(self): - """Determines if the skill has been fully loaded and setup. - When True all data has been loaded and all internal state and events setup""" + def is_fully_initialized(self) -> bool: + """ + Determines if the skill has been fully loaded and setup. + When True, all data has been loaded and all internal state + and events set up. + """ return self._is_fully_initialized @property - def stop_is_implemented(self): + def stop_is_implemented(self) -> bool: + """ + True if this skill implements a `stop` method + """ return self._stop_is_implemented @property - def converse_is_implemented(self): + def converse_is_implemented(self) -> bool: + """ + True if this skill implements a `converse` method + """ return self._converse_is_implemented + @property + def core_lang(self) -> str: + """ + Get the configured default language as a BCP-47 language code. + """ + return self._core_lang + + @property + def secondary_langs(self) -> List[str]: + """ + Get the configured secondary languages; resources will be loaded for + these languages to provide support for multilingual input, in addition + to `core_lang`. A skill may override this method to specify which + languages intents are registered in. + """ + return self._secondary_langs + + @property + def native_langs(self) -> List[str]: + """ + Languages natively supported by this skill (ie, resource files available + and explicitly supported). This is equivalent to normalized + secondary_langs + core_lang. + """ + return self._native_langs + + @property + def alphanumeric_skill_id(self) -> str: + """ + Skill id converted to only alphanumeric characters and "_". + Non alphanumeric characters are converted to "_" + """ + return self._alphanumeric_skill_id + + @property + def resources(self) -> SkillResources: + """ + Get a SkillResources object for the current language. Objects are + initialized for the current language as needed. + """ + return self._resources + def activate(self): - """Bump skill to active_skill list in intent_service. + """ + Mark this skill as active and push to the top of the active skills list. This enables converse method to be called even without skill being used in last 5 minutes. """ self._activate() def deactivate(self): - """remove skill from active_skill list in intent_service. - This stops converse method from being called + """ + Mark this skill as inactive and remove from the active skills list. + This stops converse method from being called. """ self._deactivate() - def play_audio(self, filename): + def play_audio(self, filename: str): + """ + Queue and audio file for playback + @param filename: File to play + """ core_supported = False if not is_classic_core(): try: - from mycroft.version import OVOS_VERSION_BUILD, OVOS_VERSION_MINOR, OVOS_VERSION_MAJOR + from mycroft.version import OVOS_VERSION_BUILD, \ + OVOS_VERSION_MINOR, OVOS_VERSION_MAJOR if OVOS_VERSION_MAJOR >= 1 or \ OVOS_VERSION_MINOR > 0 or \ OVOS_VERSION_BUILD >= 4: core_supported = True # min version of ovos-core - except ImportError: # skills don't require core anymore, running standalone + except ImportError: + # skills don't require core anymore, running standalone core_supported = True if core_supported: message = dig_for_message() or Message("") - self.bus.emit(message.forward("mycroft.audio.queue", {"filename": filename})) + self.bus.emit(message.forward("mycroft.audio.queue", + {"filename": filename})) else: - LOG.warning("self.play_audio requires ovos-core >= 0.0.4a45, falling back to local skill playback") + LOG.warning("self.play_audio requires ovos-core >= 0.0.4a45, " + "falling back to local skill playback") play_audio(filename).wait() - @property - def core_lang(self): - """Get the configured default language.""" - return self._core_lang - - @property - def secondary_langs(self): - """Get the configured secondary languages, mycroft is not - considered to be in these languages but i will load it's resource - files. This provides initial support for multilingual input""" - return self._secondary_langs - - @property - def native_langs(self): - """Languages natively supported by core - ie, resource files available and explicitly supported + def load_lang(self, root_directory: Optional[str] = None, + lang: Optional[str] = None): """ - return self._native_langs - - @property - def alphanumeric_skill_id(self): - """skill id converted to only alphanumeric characters - Non alpha-numeric characters are converted to "_" - - Returns: - (str) String of letters + Get a SkillResources object for this skill in the requested `lang` for + resource files in the requested `root_directory`. + @param root_directory: root path to find resources (default res_dir) + @param lang: language to get resources for (default self.lang) + @return: SkillResources object """ - return self._alphanumeric_skill_id + return self._load_lang(root_directory, lang) - @property - def resources(self): - """Instantiates a ResourceFileLocator instance when needed. - a new instance is always created to ensure self.lang - reflects the active language and not the default core language + def voc_match(self, *args, **kwargs) -> Union[str, bool]: """ - return self._resources - - def load_lang(self, root_directory=None, lang=None): - """Instantiates a ResourceFileLocator instance when needed. - a new instance is always created to ensure lang - reflects the active language and not the default core language + Wraps the default `voc_match` method, but returns `False` instead of + raising FileNotFoundError when a resource can't be resolved """ - return self._load_lang(root_directory, lang) - - def voc_match(self, *args, **kwargs): try: return super().voc_match(*args, **kwargs) except FileNotFoundError: return False - def voc_list(self, voc_filename, lang=None) -> List[str]: + def voc_list(self, voc_filename: str, + lang: Optional[str] = None) -> List[str]: """ - Get vocabulary list and cache the results - - Args: - voc_filename (str): Name of vocabulary file (e.g. 'yes' for - 'res/text/en-us/yes.voc') - lang (str): Language code, defaults to self.lang - - Returns: - list: List of vocabulary found in voc_filename + Get list of vocab options for the requested resource and cache the + results for future references. + @param voc_filename: Name of vocab resource to get options for + @param lang: language to get vocab for (default self.lang) + @return: list of string vocab options """ return self._voc_list(voc_filename, lang) - def remove_voc(self, utt, voc_filename, lang=None): - """ removes any entry in .voc file from the utterance """ + def remove_voc(self, utt: str, voc_filename: str, + lang: Optional[str] = None) -> str: + """ + Removes any vocab match from the utterance. + @param utt: Utterance to evaluate + @param voc_filename: vocab resource to remove from utt + @param lang: Optional language associated with vocab and utterance + @return: string with vocab removed + """ if utt: # Check for matches against complete words for i in self.voc_list(voc_filename, lang): @@ -164,7 +201,8 @@ def remove_voc(self, utt, voc_filename, lang=None): return utt def _register_decorated(self): - """Register all intent handlers that are decorated with an intent. + """ + Register all intent handlers that are decorated with an intent. Looks for all functions that have been marked by a decorator and read the intent data from them. The intent handlers aren't the @@ -183,7 +221,14 @@ def _register_decorated(self): if hasattr(method, 'converse'): self.converse = method - def register_intent_layer(self, layer_name, intent_list): + def register_intent_layer(self, layer_name: str, + intent_list: List[Union[IntentBuilder, Intent, + str]]): + """ + Register a named intent layer. + @param layer_name: Name of intent layer to add + @param intent_list: List of intents associated with the intent layer + """ for intent_file in intent_list: if IntentBuilder is not None and isinstance(intent_file, IntentBuilder): intent = intent_file.build() @@ -195,7 +240,12 @@ def register_intent_layer(self, layer_name, intent_list): self.intent_layers.update_layer(layer_name, [name]) # killable_events support - def send_stop_signal(self, stop_event=None): + def send_stop_signal(self, stop_event: Optional[str] = None): + """ + Notify services to stop current execution + @param stop_event: optional `stop` event name to forward + """ + waiter = Event() msg = dig_for_message() or Message("mycroft.stop") # stop event execution if stop_event: @@ -212,15 +262,19 @@ def send_stop_signal(self, stop_event=None): # NOTE: mycroft does not have an event to stop recording # this attempts to force a stop by sending silence to end STT step self.bus.emit(Message('mycroft.mic.mute')) - time.sleep(1.5) # the silence from muting should make STT stop recording + waiter.wait(1.5) # the silence from muting should make STT stop recording self.bus.emit(Message('mycroft.mic.unmute')) - time.sleep(0.5) # if TTS had not yet started + # TODO: register TTS events to track state instead of guessing + waiter.wait(0.5) # if TTS had not yet started self.bus.emit(msg.forward("mycroft.audio.speech.stop")) # backwards compat alias, no functional difference class OVOSFallbackSkill(OVOSSkill): def __new__(cls, *args, **kwargs): + log_deprecation("Implement " + "`ovos_workshop.skills.fallback.FallbackSkill`", + "0.1.0") from ovos_workshop.skills.fallback import FallbackSkill return FallbackSkill(*args, **kwargs) diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 00000000..b8b902ed --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,5 @@ +ovos-core~=0.0.7 +neon-lang-plugin-libretranslate~=0.2 +adapt-parser~=0.5 +pytest +pytest-cov diff --git a/test/unittests/skills/gui/ui/test.qml b/test/__init__.py similarity index 100% rename from test/unittests/skills/gui/ui/test.qml rename to test/__init__.py diff --git a/test/unittests/skills/intent_file/vocab/en-us/test.intent b/test/unittests/__init__.py similarity index 100% rename from test/unittests/skills/intent_file/vocab/en-us/test.intent rename to test/unittests/__init__.py diff --git a/test/unittests/skills/test_active.py b/test/unittests/skills/test_active.py new file mode 100644 index 00000000..0dc1b1e3 --- /dev/null +++ b/test/unittests/skills/test_active.py @@ -0,0 +1,28 @@ +import unittest +from unittest.mock import Mock + +from ovos_utils.messagebus import FakeBus +from ovos_workshop.skills.active import ActiveSkill +from ovos_workshop.skills.base import BaseSkill + + +class ActiveSkillExample(ActiveSkill): + active = Mock() + + def make_active(self): + self.active() + ActiveSkill.make_active(self) + + +class TestActiveSkill(unittest.TestCase): + def test_skill(self): + skill = ActiveSkillExample() + self.assertIsInstance(skill, BaseSkill) + skill.bind(FakeBus()) + skill.active.assert_called_once() + self.assertTrue(skill.active) + skill.deactivate() + self.assertTrue(skill.active) + skill.handle_skill_deactivated() + self.assertTrue(skill.active) + self.assertEqual(skill.active.call_count, 2) diff --git a/test/unittests/skills/test_auto_translatable.py b/test/unittests/skills/test_auto_translatable.py new file mode 100644 index 00000000..6b0271cc --- /dev/null +++ b/test/unittests/skills/test_auto_translatable.py @@ -0,0 +1,45 @@ +import unittest + +from ovos_workshop.skills.common_query_skill import CommonQuerySkill +from ovos_workshop.skills.fallback import FallbackSkill +from ovos_workshop.skills.base import BaseSkill + + +class TestUniversalSkill(unittest.TestCase): + from ovos_workshop.skills.auto_translatable import UniversalSkill + test_skill = UniversalSkill() + + def test_00_init(self): + self.assertIsInstance(self.test_skill, self.UniversalSkill) + self.assertIsInstance(self.test_skill, BaseSkill) + + # TODO: Test other class methods + + +class TestUniversalFallbackSkill(unittest.TestCase): + from ovos_workshop.skills.auto_translatable import UniversalFallback + test_skill = UniversalFallback() + + def test_00_init(self): + self.assertIsInstance(self.test_skill, self.UniversalFallback) + self.assertIsInstance(self.test_skill, BaseSkill) + self.assertIsInstance(self.test_skill, FallbackSkill) + + # TODO: Test other class methods + + +class TestUniversalCommonQuerySkill(unittest.TestCase): + from ovos_workshop.skills.auto_translatable import UniversalCommonQuerySkill + + class UniveralCommonQueryExample(UniversalCommonQuerySkill): + def CQS_match_query_phrase(self, phrase): + pass + + test_skill = UniveralCommonQueryExample() + + def test_00_init(self): + self.assertIsInstance(self.test_skill, self.UniversalCommonQuerySkill) + self.assertIsInstance(self.test_skill, BaseSkill) + self.assertIsInstance(self.test_skill, CommonQuerySkill) + + # TODO: Test other class methods diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py new file mode 100644 index 00000000..38f19d64 --- /dev/null +++ b/test/unittests/skills/test_base.py @@ -0,0 +1,407 @@ +import unittest + +from logging import Logger +from unittest.mock import Mock, patch +from os.path import join, dirname, isdir + +from ovos_utils.messagebus import FakeBus + + +class TestBase(unittest.TestCase): + def test_is_classic_core(self): + from ovos_workshop.skills.base import is_classic_core + self.assertIsInstance(is_classic_core(), bool) + + def test_simple_trace(self): + from ovos_workshop.skills.base import simple_trace + trace = ["line_1\n", " line_2 \n", " \n", "line_3 \n"] + self.assertEqual(simple_trace(trace), "Traceback:\nline_1\n line_2 \n") + + +class TestBaseSkill(unittest.TestCase): + from ovos_workshop.skills.base import BaseSkill + bus = FakeBus() + skill_id = "test_base_skill" + skill = BaseSkill(bus=bus, skill_id=skill_id) + + def test_00_skill_init(self): + from ovos_workshop.settings import SkillSettingsManager + from ovos_workshop.skills.base import SkillGUI + from ovos_utils.events import EventContainer, EventSchedulerInterface + from ovos_utils.intents import IntentServiceInterface + from ovos_utils.process_utils import RuntimeRequirements + from ovos_utils.enclosure.api import EnclosureAPI + from ovos_workshop.filesystem import FileSystemAccess + from ovos_workshop.resource_files import SkillResources + + self.assertIsInstance(self.skill.log, Logger) + self.assertIsInstance(self.skill._enable_settings_manager, bool) + self.assertEqual(self.skill.name, self.skill.__class__.__name__) + self.assertEqual(self.skill.skill_id, self.skill_id) + self.assertIsInstance(self.skill.settings_manager, SkillSettingsManager) + self.assertTrue(isdir(self.skill.root_dir)) + self.assertEqual(self.skill.res_dir, self.skill.root_dir) + self.assertIsInstance(self.skill.gui, SkillGUI) + self.assertIsInstance(self.skill.config_core, dict) + self.assertIsNone(self.skill.settings_change_callback) + self.assertTrue(self.skill.reload_skill) + self.assertIsInstance(self.skill.events, EventContainer) + self.assertEqual(self.skill.events.bus, self.bus) + self.assertIsInstance(self.skill.event_scheduler, + EventSchedulerInterface) + self.assertIsInstance(self.skill.intent_service, IntentServiceInterface) + + self.assertIsInstance(self.skill.runtime_requirements, + RuntimeRequirements) + self.assertIsInstance(self.skill.voc_match_cache, dict) + self.assertTrue(self.skill._is_fully_initialized) + self.assertTrue(isdir(dirname(self.skill._settings_path))) + self.assertIsInstance(self.skill.settings, dict) + self.assertIsNone(self.skill.dialog_renderer) + self.assertIsInstance(self.skill.enclosure, EnclosureAPI) + self.assertIsInstance(self.skill.file_system, FileSystemAccess) + self.assertTrue(isdir(self.skill.file_system.path)) + self.assertEqual(self.skill.bus, self.bus) + self.assertIsInstance(self.skill.location, dict) + self.assertIsInstance(self.skill.location_pretty, str) + self.assertIsInstance(self.skill.location_timezone, str) + self.assertIsInstance(self.skill.lang, str) + self.assertEqual(len(self.skill.lang.split('-')), 2) + self.assertEqual(self.skill._core_lang, self.skill.lang) + self.assertIsInstance(self.skill._secondary_langs, list) + self.assertIsInstance(self.skill._native_langs, list) + self.assertIn(self.skill._core_lang, self.skill._native_langs) + self.assertIsInstance(self.skill._alphanumeric_skill_id, str) + self.assertIsInstance(self.skill._resources, SkillResources) + self.assertEqual(self.skill._resources.language, self.skill.lang) + self.assertFalse(self.skill._stop_is_implemented) + self.assertFalse(self.skill._converse_is_implemented) + + def test_handle_first_run(self): + # TODO + pass + + def test_check_for_first_run(self): + # TODO + pass + + def test_startup(self): + # TODO + pass + + def test_init_settings(self): + # TODO + pass + + def test_init_skill_gui(self): + # TODO + pass + + def test_init_settings_manager(self): + # TODO + pass + + def test_start_filewatcher(self): + # TODO + pass + + def test_upload_settings(self): + # TODO + pass + + def test_handle_settings_file_change(self): + # TODO + pass + + def test_load_lang(self): + # TODO + pass + + def test_bind(self): + # TODO + pass + + def test_register_public_api(self): + # TODO + pass + + def test_register_system_event_handlers(self): + # TODO + pass + + def test_handle_settings_change(self): + # TODO + pass + + def test_detach(self): + # TODO + pass + + def test_send_public_api(self): + # TODO + pass + + def test_get_intro_message(self): + self.assertIsInstance(self.skill.get_intro_message(), str) + self.assertFalse(self.skill.get_intro_message()) + + def test_handle_skill_activated(self): + # TODO + pass + + def test_handle_skill_deactivated(self): + # TODO + pass + + def test_activate(self): + # TODO + pass + + def test_deactivate(self): + # TODO + pass + + def test_handle_converse_ack(self): + # TODO + pass + + def test_handle_converse_request(self): + # TODO + pass + + def test_converse(self): + # TODO + self.assertFalse(self.skill.converse()) + + # TODO port get_response methods per #69 + + def test_ask_yesno(self): + # TODO + pass + + def test_ask_selection(self): + # TODO + pass + + def test_voc_list(self): + # TODO + pass + + def test_voc_match(self): + # TODO + pass + + def test_report_metric(self): + # TODO + pass + + def test_send_email(self): + # TODO + pass + + def test_handle_collect_resting(self): + # TODO + pass + + def test_register_resting_screen(self): + # TODO + pass + + def test_register_decorated(self): + # TODO + pass + + def test_find_resource(self): + # TODO + pass + + def test_on_event_start(self): + # TODO + pass + + def test_on_event_end(self): + # TODO + pass + + def test_on_event_error(self): + # TODO + pass + + def test_add_event(self): + # TODO + pass + + def test_remove_event(self): + # TODO + pass + + def test_register_adapt_intent(self): + # TODO + pass + + def test_register_intent(self): + # TODO + pass + + def test_register_intent_file(self): + # TODO + pass + + def test_register_entity_file(self): + # TODO + pass + + def test_handle_enable_intent(self): + # TODO + pass + + def test_handle_disable_intent(self): + # TODO + pass + + def test_disable_intent(self): + # TODO + pass + + def test_enable_intent(self): + # TODO + pass + + def test_set_context(self): + # TODO + pass + + def test_remove_context(self): + # TODO + pass + + def test_handle_set_cross_context(self): + # TODO + pass + + def test_handle_remove_cross_context(self): + # TODO + pass + + def test_set_cross_skill_contest(self): + # TODO + pass + + def test_remove_cross_skill_context(self): + # TODO + pass + + def test_register_vocabulary(self): + # TODO + pass + + def test_register_regex(self): + # TODO + pass + + def test_speak(self): + # TODO + pass + + def test_speak_dialog(self): + # TODO + pass + + def test_acknowledge(self): + # TODO + pass + + def test_load_dialog_files(self): + # TODO + pass + + def test_load_data_files(self): + # TODO + pass + + def test_load_vocab_files(self): + # TODO + pass + + def test_load_regex_files(self): + # TODO + pass + + def test_handle_stop(self): + # TODO + pass + + def test_stop(self): + self.skill.stop() + + def test_shutdown(self): + self.skill.shutdown() + + def test_default_shutdown(self): + # TODO + pass + + def test_schedule_event(self): + # TODO + pass + + def test_schedule_repeating_event(self): + # TODO + pass + + def test_update_scheduled_event(self): + # TODO + pass + + def test_cancel_scheduled_event(self): + # TODO + pass + + def test_get_scheduled_event_status(self): + # TODO + pass + + def test_cancel_all_repeating_events(self): + # TODO + pass + + +class TestSkillGui(unittest.TestCase): + class LegacySkill(Mock): + skill_id = "old_skill" + bus = FakeBus() + config_core = {"gui": {"test": True, + "legacy": True}} + root_dir = join(dirname(__file__), "test_gui/gui") + + class GuiSkill(Mock): + skill_id = "new_skill" + bus = FakeBus() + config_core = {"gui": {"test": True, + "legacy": False}} + root_dir = join(dirname(__file__), "test_gui") + + @patch("ovos_workshop.skills.base.GUIInterface.__init__") + def test_skill_gui(self, interface_init): + from ovos_utils.gui import GUIInterface + from ovos_workshop.skills.base import SkillGUI + + # Old skill with `ui` directory in root + old_skill = self.LegacySkill() + old_gui = SkillGUI(old_skill) + self.assertEqual(old_gui.skill, old_skill) + self.assertIsInstance(old_gui, GUIInterface) + interface_init.assert_called_once_with( + old_gui, skill_id=old_skill.skill_id, bus=old_skill.bus, + config=old_skill.config_core['gui'], + ui_directories={"qt5": join(old_skill.root_dir, "ui")}) + + # New skill with `gui` directory in root + new_skill = self.GuiSkill() + new_gui = SkillGUI(new_skill) + self.assertEqual(new_gui.skill, new_skill) + self.assertIsInstance(new_gui, GUIInterface) + interface_init.assert_called_with( + new_gui, skill_id=new_skill.skill_id, bus=new_skill.bus, + config=new_skill.config_core['gui'], + ui_directories={"all": join(new_skill.root_dir, "gui")}) diff --git a/test/unittests/skills/test_fallback_skill.py b/test/unittests/skills/test_fallback_skill.py index ffa7450e..0188a82b 100644 --- a/test/unittests/skills/test_fallback_skill.py +++ b/test/unittests/skills/test_fallback_skill.py @@ -1,53 +1,234 @@ -from unittest import TestCase, mock +from unittest import TestCase +from unittest.mock import patch -from ovos_workshop.skills.fallback import FallbackSkillV1 as FallbackSkill +from ovos_utils.messagebus import FakeBus +from ovos_workshop.decorators import fallback_handler +from ovos_workshop.skills.base import BaseSkill +from ovos_workshop.skills.fallback import FallbackSkillV1, FallbackSkillV2, \ + FallbackSkill -def setup_fallback(fb_class): - fb_skill = fb_class() - fb_skill.bind(mock.Mock(name='bus')) - fb_skill.initialize() - return fb_skill +class SimpleFallback(FallbackSkillV1): + """Simple fallback skill used for test.""" + def initialize(self): + self.register_fallback(self.fallback_handler, 42) + + def fallback_handler(self, _): + pass + + +class V2FallbackSkill(FallbackSkillV2): + def __init__(self): + FallbackSkillV2.__init__(self, FakeBus(), "fallback_v2") + + @fallback_handler + def handle_fallback(self, message): + pass + + @fallback_handler(10) + def high_prio_fallback(self, message): + pass class TestFallbackSkill(TestCase): - def test_life_cycle(self): - """Test startup and shutdown of a fallback skill. + # TODO: Test `__new__` logic + pass + + def test_class_inheritance(self): + from ovos_workshop.skills.ovos import OVOSSkill + from ovos_workshop.skills.mycroft_skill import MycroftSkill + fallback = FallbackSkill("test") + self.assertIsInstance(fallback, BaseSkill) + self.assertIsInstance(fallback, OVOSSkill) + self.assertIsInstance(fallback, MycroftSkill) + self.assertIsInstance(fallback, FallbackSkillV1) + self.assertIsInstance(fallback, FallbackSkillV2) + self.assertIsInstance(fallback, FallbackSkill) + + +class TestFallbackSkillV1(TestCase): + @staticmethod + def setup_fallback(fb_class): + fb_skill = fb_class() + fb_skill.bind(FakeBus()) + fb_skill.initialize() + return fb_skill + + def test_inheritance(self): + from ovos_workshop.skills.ovos import OVOSSkill + from ovos_workshop.skills.mycroft_skill import MycroftSkill + fallback = FallbackSkillV1("test") + self.assertIsInstance(fallback, BaseSkill) + self.assertIsInstance(fallback, OVOSSkill) + self.assertIsInstance(fallback, MycroftSkill) + self.assertIsInstance(fallback, FallbackSkillV1) + self.assertIsInstance(fallback, FallbackSkillV2) + self.assertIsInstance(fallback, FallbackSkill) + + def test_make_intent_failure_handler(self): + # TODO + pass + + def test_report_timing(self): + # TODO + pass + + def test__register_fallback(self): + # TODO + pass + + def test_register_fallback(self): + # TODO + pass + + def test_remove_registered_handler(self): + # TODO + pass + + @patch("ovos_workshop.skills.fallback.FallbackSkillV1." + "_remove_registered_handler") + def test_remove_fallback(self, remove_handler): + def wrapper(handler): + def wrapped(): + if handler(): + return True + return False + return wrapped + + def _mock_1(): + pass + + def _mock_2(): + pass + + FallbackSkillV1.wrapper_map.append((_mock_1, wrapper(_mock_1))) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) + + FallbackSkillV1.wrapper_map.append((_mock_2, wrapper(_mock_2))) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 2) + + # Successful remove existing wrapper + remove_handler.return_value = True + self.assertTrue(FallbackSkillV1.remove_fallback(_mock_1)) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) + self.assertFalse(FallbackSkillV1.remove_fallback(_mock_1)) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) + # Failed remove existing wrapper + remove_handler.return_value = False + self.assertFalse(FallbackSkillV1.remove_fallback( + FallbackSkillV1.wrapper_map[0][1])) + self.assertEqual(FallbackSkillV1.wrapper_map, []) + + def test_remove_instance_handlers(self): + # TODO + pass + + def test_default_shutdown(self): + # TODO + pass + + def test_register_decorated(self): + # TODO + pass + + def test_life_cycle(self): + """ + Test startup and shutdown of a fallback skill. Ensure that an added handler is removed as part of default shutdown. """ - self.assertEqual(len(FallbackSkill.fallback_handlers), 0) - fb_skill = setup_fallback(SimpleFallback) - self.assertEqual(len(FallbackSkill.fallback_handlers), 1) - self.assertEqual(FallbackSkill.wrapper_map[0][0], + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) + fb_skill = self.setup_fallback(SimpleFallback) + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 1) + self.assertEqual(FallbackSkillV1.wrapper_map[0][0], fb_skill.fallback_handler) - self.assertEqual(len(FallbackSkill.wrapper_map), 1) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 1) fb_skill.default_shutdown() - self.assertEqual(len(FallbackSkill.fallback_handlers), 0) - self.assertEqual(len(FallbackSkill.wrapper_map), 0) + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 0) def test_manual_removal(self): - """Test that the call to remove_fallback() removes the handler""" - self.assertEqual(len(FallbackSkill.fallback_handlers), 0) + """ + Test that the call to remove_fallback() removes the handler + """ + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) # Create skill adding a single handler - fb_skill = setup_fallback(SimpleFallback) - self.assertEqual(len(FallbackSkill.fallback_handlers), 1) + fb_skill = self.setup_fallback(SimpleFallback) + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 1) self.assertTrue(fb_skill.remove_fallback(fb_skill.fallback_handler)) # Both internal trackers of handlers should be cleared now - self.assertEqual(len(FallbackSkill.fallback_handlers), 0) - self.assertEqual(len(FallbackSkill.wrapper_map), 0) + self.assertEqual(len(FallbackSkillV1.fallback_handlers), 0) + self.assertEqual(len(FallbackSkillV1.wrapper_map), 0) # Removing after it's already been removed should fail self.assertFalse(fb_skill.remove_fallback(fb_skill.fallback_handler)) -class SimpleFallback(FallbackSkill): - """Simple fallback skill used for test.""" - def initialize(self): - self.register_fallback(self.fallback_handler, 42) +class TestFallbackSkillV2(TestCase): + fallback_skill = FallbackSkillV2(FakeBus(), "test_fallback_v2") + + def test_class_inheritance(self): + from ovos_workshop.skills.ovos import OVOSSkill + from ovos_workshop.skills.mycroft_skill import MycroftSkill + self.assertIsInstance(self.fallback_skill, BaseSkill) + self.assertIsInstance(self.fallback_skill, OVOSSkill) + self.assertIsInstance(self.fallback_skill, MycroftSkill) + self.assertIsInstance(self.fallback_skill, FallbackSkillV1) + self.assertIsInstance(self.fallback_skill, FallbackSkillV2) + self.assertIsInstance(self.fallback_skill, FallbackSkill) + + def test_00_init(self): + self.assertIsInstance(self.fallback_skill, FallbackSkillV2) + self.assertIsInstance(self.fallback_skill, FallbackSkill) + self.assertIsInstance(self.fallback_skill, BaseSkill) + + def test_priority(self): + FallbackSkillV2.fallback_config = {} + + # No config or handlers + self.assertEqual(self.fallback_skill.priority, 101) + # Config override + FallbackSkillV2.fallback_config = \ + {"fallback_priorities": {"test_fallback_v2": 10}} + self.assertEqual(self.fallback_skill.priority, 10, + self.fallback_skill.fallback_config) + + fallback_skill = V2FallbackSkill() + + # Minimum handler + self.assertEqual(fallback_skill.priority, 10) + # Config override + FallbackSkillV2.fallback_config['fallback_priorities'][ + fallback_skill.skill_id] = 80 + self.assertEqual(fallback_skill.priority, 80) + + def test_can_answer(self): + self.assertFalse(self.fallback_skill.can_answer([""], "en-us")) + # TODO + + def test_register_system_event_handlers(self): + # TODO + pass + + def test_handle_fallback_ack(self): + # TODO + pass + + def test_handle_fallback_request(self): + # TODO + pass + + def test_register_fallback(self): + # TODO + pass + + def test_default_shutdown(self): + # TODO + pass - def fallback_handler(self): + def test_register_decorated(self): + # TODO pass diff --git a/test/unittests/skills/intent_file/vocab/en-us/test_ent.entity b/test/unittests/skills/test_gui/gui/ui/test.qml similarity index 100% rename from test/unittests/skills/intent_file/vocab/en-us/test_ent.entity rename to test/unittests/skills/test_gui/gui/ui/test.qml diff --git a/test/unittests/skills/test_idle_display_skill.py b/test/unittests/skills/test_idle_display_skill.py new file mode 100644 index 00000000..93b6026e --- /dev/null +++ b/test/unittests/skills/test_idle_display_skill.py @@ -0,0 +1,14 @@ +import unittest + +from ovos_utils.messagebus import FakeBus +from ovos_workshop.skills.base import BaseSkill +from ovos_workshop.skills.idle_display_skill import IdleDisplaySkill + + +class TestIdleDisplaySkill(unittest.TestCase): + skill = IdleDisplaySkill(bus=FakeBus(), skill_id="test_idle_skill") + + def test_00_skill_init(self): + self.assertIsInstance(self.skill, BaseSkill) + self.assertIsInstance(self.skill, IdleDisplaySkill) + # TODO: Implement more tests \ No newline at end of file diff --git a/test/unittests/skills/test_mycroft_skill/__init__.py b/test/unittests/skills/test_mycroft_skill/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent b/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test.intent new file mode 100644 index 00000000..e69de29b diff --git a/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity b/test/unittests/skills/test_mycroft_skill/intent_file/vocab/en-us/test_ent.entity new file mode 100644 index 00000000..e69de29b diff --git a/test/unittests/skills/locale/en-us/turn_off2_test.voc b/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc similarity index 100% rename from test/unittests/skills/locale/en-us/turn_off2_test.voc rename to test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off2_test.voc diff --git a/test/unittests/skills/locale/en-us/turn_off_test.voc b/test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc similarity index 100% rename from test/unittests/skills/locale/en-us/turn_off_test.voc rename to test/unittests/skills/test_mycroft_skill/locale/en-us/turn_off_test.voc diff --git a/test/unittests/skills/mocks.py b/test/unittests/skills/test_mycroft_skill/mocks.py similarity index 100% rename from test/unittests/skills/mocks.py rename to test/unittests/skills/test_mycroft_skill/mocks.py diff --git a/test/unittests/skills/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py similarity index 99% rename from test/unittests/skills/test_mycroft_skill.py rename to test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py index 32d87fd6..c5f1a639 100644 --- a/test/unittests/skills/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py @@ -20,12 +20,12 @@ from os.path import join, dirname, abspath from unittest.mock import MagicMock, patch -#import pytest from ovos_utils.intents import IntentBuilder from ovos_bus_client import Message from ovos_config.config import Configuration -from ovos_workshop.decorators import intent_handler, resting_screen_handler, intent_file_handler +from ovos_workshop.decorators import intent_handler, resting_screen_handler, \ + intent_file_handler from ovos_workshop.skills.mycroft_skill import MycroftSkill from .mocks import base_config @@ -247,7 +247,6 @@ def check_register_decorators(self, result_list): sorted(result_list, key=lambda d: sorted(d.items()))) self.emitter.reset() - #@pytest.mark.skip def test_register_decorators(self): """ Test decorated intents """ path_orig = sys.path @@ -266,7 +265,8 @@ def test_register_decorators(self): 'vocab', 'en-us', 'test.intent'), 'lang': 'en-us', 'samples': [], - 'name': str(s.skill_id) + ':test.intent'}] + 'name': str(s.skill_id) + ':test.intent'} + ] self.check_register_decorators(expected) # Restore sys.path diff --git a/test/unittests/skills/test_mycroft_skill_get_response.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py similarity index 100% rename from test/unittests/skills/test_mycroft_skill_get_response.py rename to test/unittests/skills/test_mycroft_skill/test_mycroft_skill_get_response.py diff --git a/test/unittests/skills/test_skill/__init__.py b/test/unittests/skills/test_mycroft_skill/test_skill/__init__.py similarity index 100% rename from test/unittests/skills/test_skill/__init__.py rename to test/unittests/skills/test_mycroft_skill/test_skill/__init__.py diff --git a/test/unittests/skills/test_skill/dialog/en-us/what do you want.dialog b/test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog similarity index 100% rename from test/unittests/skills/test_skill/dialog/en-us/what do you want.dialog rename to test/unittests/skills/test_mycroft_skill/test_skill/dialog/en-us/what do you want.dialog diff --git a/test/unittests/skills/translate/in-dialog/dialog/en-us/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list similarity index 100% rename from test/unittests/skills/translate/in-dialog/dialog/en-us/good_things.list rename to test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/good_things.list diff --git a/test/unittests/skills/translate/in-dialog/dialog/en-us/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value similarity index 100% rename from test/unittests/skills/translate/in-dialog/dialog/en-us/named_things.value rename to test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/named_things.value diff --git a/test/unittests/skills/translate/in-dialog/dialog/en-us/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template similarity index 100% rename from test/unittests/skills/translate/in-dialog/dialog/en-us/test.template rename to test/unittests/skills/test_mycroft_skill/translate/in-dialog/dialog/en-us/test.template diff --git a/test/unittests/skills/translate/in-locale/locale/de-de/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/de-de/good_things.list rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/good_things.list diff --git a/test/unittests/skills/translate/in-locale/locale/de-de/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/de-de/named_things.value rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/named_things.value diff --git a/test/unittests/skills/translate/in-locale/locale/de-de/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/de-de/test.template rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/de-de/test.template diff --git a/test/unittests/skills/translate/in-locale/locale/en-us/good_things.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/en-us/good_things.list rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/good_things.list diff --git a/test/unittests/skills/translate/in-locale/locale/en-us/named_things.value b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/en-us/named_things.value rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/named_things.value diff --git a/test/unittests/skills/translate/in-locale/locale/en-us/not_in_german.list b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/en-us/not_in_german.list rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/not_in_german.list diff --git a/test/unittests/skills/translate/in-locale/locale/en-us/test.template b/test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template similarity index 100% rename from test/unittests/skills/translate/in-locale/locale/en-us/test.template rename to test/unittests/skills/test_mycroft_skill/translate/in-locale/locale/en-us/test.template diff --git a/test/unittests/test_skill_classes.py b/test/unittests/skills/test_ovos.py similarity index 50% rename from test/unittests/test_skill_classes.py rename to test/unittests/skills/test_ovos.py index 9492d7ff..d43ab520 100644 --- a/test/unittests/test_skill_classes.py +++ b/test/unittests/skills/test_ovos.py @@ -1,13 +1,13 @@ import unittest -from unittest.mock import Mock -from ovos_workshop import OVOSAbstractApplication -from ovos_workshop.decorators import classproperty -from ovos_workshop.skills.ovos import OVOSSkill from ovos_utils.process_utils import RuntimeRequirements -from ovos_workshop.skills.mycroft_skill import is_classic_core from ovos_utils.messagebus import FakeBus +from ovos_utils import classproperty +from ovos_workshop import IntentLayers +from ovos_workshop.resource_files import SkillResources + from ovos_workshop.settings import SkillSettingsManager +from ovos_workshop.skills.ovos import OVOSSkill class OfflineSkill(OVOSSkill): @@ -33,39 +33,87 @@ def runtime_requirements(self): no_network_fallback=False) -class TestSkill(OVOSSkill): +class MockSkill(OVOSSkill): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) -class TestApplication(OVOSAbstractApplication): - def __init__(self, *args, **kwargs): - super().__init__(skill_id="Test Application", *args, **kwargs) - - -class TestSkills(unittest.TestCase): +class TestOVOSSkill(unittest.TestCase): + bus = FakeBus() + skill = OVOSSkill(bus=bus, skill_id="test_ovos_skill") + + def test_00_skill_init(self): + from ovos_utils.skills.audioservice import AudioServiceInterface + self.assertIsInstance(self.skill.private_settings, dict) + self.assertIsInstance(self.skill._threads, list) + self.assertIsNotNone(self.skill._original_converse) + self.assertIsInstance(self.skill.intent_layers, IntentLayers) + self.assertIsInstance(self.skill.audio_service, AudioServiceInterface) + self.assertTrue(self.skill.is_fully_initialized) + self.assertFalse(self.skill.stop_is_implemented) + self.assertFalse(self.skill.converse_is_implemented) + self.assertIsInstance(self.skill.core_lang, str) + self.assertIsInstance(self.skill.secondary_langs, list) + self.assertIsInstance(self.skill.native_langs, list) + self.assertIsInstance(self.skill.alphanumeric_skill_id, str) + self.assertIsInstance(self.skill.resources, SkillResources) + + def test_activate(self): + # TODO + pass + + def test_deactivate(self): + # TODO + pass + + def test_play_audio(self): + # TODO + pass + + def test_load_lang(self): + # TODO + pass + + def test_voc_match(self): + # TODO + pass + + def test_voc_list(self): + # TODO + pass + + def test_remove_voc(self): + # TODO + pass + + def test_register_decorated(self): + # TODO + pass + + def test_register_intent_layer(self): + # TODO + pass + + def test_send_stop_signal(self): + # TODO + pass def test_settings_manager_init(self): bus = FakeBus() - skill_default = TestSkill(bus=bus) + skill_default = MockSkill(bus=bus) skill_default._startup(bus) - # This doesn't apply to `mycroft-core`, only `ovos-core` - if not is_classic_core(): - self.assertIsInstance(skill_default.settings_manager, SkillSettingsManager) - skill_disabled_settings = TestSkill(bus=bus, - enable_settings_manager=False) - skill_disabled_settings._startup(bus) - self.assertIsNone(skill_disabled_settings.settings_manager) + self.assertIsInstance(skill_default.settings_manager, + SkillSettingsManager) - plugin = TestApplication(bus=bus) - plugin._startup(bus) - self.assertIsNone(plugin.settings_manager) + skill_disabled_settings = MockSkill(bus=bus, + enable_settings_manager=False) + skill_disabled_settings._startup(bus) + self.assertIsNone(skill_disabled_settings.settings_manager) def test_bus_setter(self): - from ovos_utils.messagebus import FakeBus bus = FakeBus() - skill = TestSkill() + skill = MockSkill() skill._startup(bus) self.assertEqual(skill.bus, bus) new_bus = FakeBus() @@ -74,7 +122,7 @@ def test_bus_setter(self): with self.assertRaises(TypeError): skill.bus = None - def test_class_property(self): + def test_runtime_requirements(self): self.assertEqual(OfflineSkill.runtime_requirements, RuntimeRequirements(internet_before_load=False, network_before_load=False, @@ -92,57 +140,17 @@ def test_class_property(self): no_network_fallback=False) ) self.assertEqual(OVOSSkill.runtime_requirements, - RuntimeRequirements() - ) + RuntimeRequirements()) def test_class_inheritance(self): from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.ovos import OVOSSkill from ovos_workshop.skills.mycroft_skill import MycroftSkill - from ovos_workshop.skills.fallback import FallbackSkill, FallbackSkillV2, FallbackSkillV1 from ovos_workshop.app import OVOSAbstractApplication - skill = TestSkill() + skill = MockSkill() self.assertIsInstance(skill, BaseSkill) self.assertIsInstance(skill, OVOSSkill) self.assertIsInstance(skill, MycroftSkill) self.assertNotIsInstance(skill, OVOSAbstractApplication) - app = TestApplication() - self.assertIsInstance(app, BaseSkill) - self.assertIsInstance(app, OVOSSkill) - self.assertIsInstance(app, MycroftSkill) - self.assertIsInstance(app, OVOSAbstractApplication) - - mycroft_skill = MycroftSkill() - self.assertIsInstance(mycroft_skill, BaseSkill) - self.assertIsInstance(mycroft_skill, MycroftSkill) - self.assertNotIsInstance(mycroft_skill, OVOSSkill) - self.assertNotIsInstance(mycroft_skill, OVOSAbstractApplication) - - fallback = FallbackSkill("test") - self.assertIsInstance(fallback, BaseSkill) - self.assertIsInstance(fallback, OVOSSkill) - self.assertIsInstance(fallback, MycroftSkill) - self.assertIsInstance(fallback, FallbackSkillV1) - self.assertIsInstance(fallback, FallbackSkillV2) - self.assertIsInstance(fallback, FallbackSkill) - self.assertNotIsInstance(fallback, OVOSAbstractApplication) - - fallback = FallbackSkillV1("test") - self.assertIsInstance(fallback, BaseSkill) - self.assertIsInstance(fallback, OVOSSkill) - self.assertIsInstance(fallback, MycroftSkill) - self.assertIsInstance(fallback, FallbackSkillV1) - self.assertIsInstance(fallback, FallbackSkillV2) - self.assertIsInstance(fallback, FallbackSkill) - self.assertNotIsInstance(fallback, OVOSAbstractApplication) - - fallback = FallbackSkillV2("test") - self.assertIsInstance(fallback, BaseSkill) - self.assertIsInstance(fallback, OVOSSkill) - self.assertIsInstance(fallback, MycroftSkill) - self.assertIsInstance(fallback, FallbackSkillV1) - self.assertIsInstance(fallback, FallbackSkillV2) - self.assertIsInstance(fallback, FallbackSkill) - self.assertNotIsInstance(fallback, OVOSAbstractApplication) diff --git a/test/unittests/test_abstract_app.py b/test/unittests/test_abstract_app.py index 17773288..d743ce2b 100644 --- a/test/unittests/test_abstract_app.py +++ b/test/unittests/test_abstract_app.py @@ -31,6 +31,9 @@ def setUpClass(cls) -> None: settings=cls.settings_obj, gui=cls.gui) cls.app._startup(cls.bus) + def test_settings_manager_init(self): + self.assertIsNone(self.app.settings_manager) + def test_settings_init(self): self.assertNotEqual(self.app.settings, self.settings_obj) self.assertFalse(self.app.settings['__mycroft_skill_firstrun']) @@ -80,3 +83,14 @@ def test_settings_path(self): # Cleanup test files remove(test_app._settings_path) remove(test_skill._settings_path) + + def test_class_inheritance(self): + from ovos_workshop.skills.base import BaseSkill + from ovos_workshop.skills.ovos import OVOSSkill + from ovos_workshop.skills.mycroft_skill import MycroftSkill + from ovos_workshop.app import OVOSAbstractApplication + + self.assertIsInstance(self.app, BaseSkill) + self.assertIsInstance(self.app, OVOSSkill) + self.assertIsInstance(self.app, MycroftSkill) + self.assertIsInstance(self.app, OVOSAbstractApplication) diff --git a/test/unittests/test_skill.py b/test/unittests/test_skill.py index 7e15151a..c1f25234 100644 --- a/test/unittests/test_skill.py +++ b/test/unittests/test_skill.py @@ -1,6 +1,6 @@ import json import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock from ovos_bus_client import Message @@ -8,7 +8,7 @@ from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core from mycroft.skills import MycroftSkill as CoreSkill from ovos_utils.messagebus import FakeBus -from os.path import dirname, join +from os.path import dirname from ovos_workshop.skill_launcher import SkillLoader @@ -207,44 +207,3 @@ def test_load(self): self.assertEqual(args.skill_id, "args") self.assertEqual(args.bus, bus) self.assertEqual(args.gui, gui) - - -class TestSkillGui(unittest.TestCase): - class LegacySkill(Mock): - skill_id = "old_skill" - bus = FakeBus() - config_core = {"gui": {"test": True, - "legacy": True}} - root_dir = join(dirname(__file__), "skills", "gui") - - class GuiSkill(Mock): - skill_id = "new_skill" - bus = FakeBus() - config_core = {"gui": {"test": True, - "legacy": False}} - root_dir = join(dirname(__file__), "skills") - - @patch("ovos_workshop.skills.base.GUIInterface.__init__") - def test_skill_gui(self, interface_init): - from ovos_utils.gui import GUIInterface - from ovos_workshop.skills.base import SkillGUI - - # Old skill with `ui` directory in root - old_skill = self.LegacySkill() - old_gui = SkillGUI(old_skill) - self.assertEqual(old_gui.skill, old_skill) - self.assertIsInstance(old_gui, GUIInterface) - interface_init.assert_called_once_with( - old_gui, skill_id=old_skill.skill_id, bus=old_skill.bus, - config=old_skill.config_core['gui'], - ui_directories={"qt5": join(old_skill.root_dir, "ui")}) - - # New skill with `gui` directory in root - new_skill = self.GuiSkill() - new_gui = SkillGUI(new_skill) - self.assertEqual(new_gui.skill, new_skill) - self.assertIsInstance(new_gui, GUIInterface) - interface_init.assert_called_with( - new_gui, skill_id=new_skill.skill_id, bus=new_skill.bus, - config=new_skill.config_core['gui'], - ui_directories={"all": join(new_skill.root_dir, "gui")}) diff --git a/test/unittests/test_skill_launcher.py b/test/unittests/test_skill_launcher.py index d865e416..187d113c 100644 --- a/test/unittests/test_skill_launcher.py +++ b/test/unittests/test_skill_launcher.py @@ -58,7 +58,7 @@ def test_remove_submodule_refs(self): def test_load_skill_module(self): from ovos_workshop.skill_launcher import load_skill_module - test_path = join(dirname(__file__), "skills", "test_skill", + test_path = join(dirname(__file__), "ovos_tskill_abort", "__init__.py") skill_id = "test_skill.test" module = load_skill_module(test_path, skill_id) @@ -70,7 +70,7 @@ def test_get_skill_class(self): from ovos_workshop.skill_launcher import get_skill_class, \ load_skill_module from ovos_workshop.skills.mycroft_skill import _SkillMetaclass - test_path = join(dirname(__file__), "skills", "test_skill", + test_path = join(dirname(__file__), "ovos_tskill_abort", "__init__.py") skill_id = "test_skill.test" module = load_skill_module(test_path, skill_id) @@ -85,7 +85,7 @@ def test_get_skill_class(self): def test_get_create_skill_function(self): from ovos_workshop.skill_launcher import get_create_skill_function, \ load_skill_module - test_path = join(dirname(__file__), "skills", "test_skill", + test_path = join(dirname(__file__), "ovos_tskill_abort", "__init__.py") skill_id = "test_skill.test" module = load_skill_module(test_path, skill_id) From 72f795d6de8db3d0b9822a7d163693440ef78689 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 10 Jul 2023 18:10:06 +0000 Subject: [PATCH 109/154] Increment Version to 0.0.12a36 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 36ba0515..19250349 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 35 +VERSION_ALPHA = 36 # END_VERSION_BLOCK From cb973a165f240f359d406a2815eebd7fea8be914 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 10 Jul 2023 18:10:40 +0000 Subject: [PATCH 110/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc4d4fe..5a8ad821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a35](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a35) (2023-07-07) +## [0.0.12a36](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a36) (2023-07-10) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a34...0.0.12a35) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a35...0.0.12a36) + +**Implemented enhancements:** + +- Skills module tests, docstrings, and annotations [\#108](https://github.com/OpenVoiceOS/OVOS-workshop/pull/108) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a35](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a35) (2023-07-07) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a34...V0.0.12a35) **Merged pull requests:** From 5b56a11f73bd6e01b3ec30b6cb89d3cd79a65ae0 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Mon, 10 Jul 2023 12:08:14 -0700 Subject: [PATCH 111/154] Add docstrings and unit tests for app.py (#110) --- ovos_workshop/app.py | 66 +++++++++++++++++++++-------- test/unittests/test_abstract_app.py | 23 ++++++++++ 2 files changed, 72 insertions(+), 17 deletions(-) diff --git a/ovos_workshop/app.py b/ovos_workshop/app.py index 6e0a5208..d49e9fc1 100644 --- a/ovos_workshop/app.py +++ b/ovos_workshop/app.py @@ -1,16 +1,33 @@ from os.path import isdir, join - +from typing import Optional from ovos_config.locations import get_xdg_config_save_path from ovos_utils.messagebus import get_mycroft_bus -from ovos_utils.log import LOG - +from ovos_utils.log import log_deprecation +from ovos_utils.gui import GUIInterface +from ovos_bus_client.client.client import MessageBusClient from ovos_workshop.resource_files import locate_lang_directories from ovos_workshop.skills.ovos import OVOSSkill class OVOSAbstractApplication(OVOSSkill): - def __init__(self, skill_id, bus=None, resources_dir=None, - lang=None, settings=None, gui=None, enable_settings_manager=False): + def __init__(self, skill_id: str, bus: Optional[MessageBusClient] = None, + resources_dir: Optional[str] = None, + lang=None, settings: Optional[dict] = None, + gui: Optional[GUIInterface] = None, + enable_settings_manager: bool = False): + """ + Create an Application. An application is essentially a skill, but + designed such that it may be run without an intent service. + @param skill_id: Unique ID for this application + @param bus: MessageBusClient to bind to application + @param resources_dir: optional root resource directory (else defaults to + application `root_dir` + @param lang: DEPRECATED language of the application + @param settings: DEPRECATED settings object + @param gui: GUIInterface to bind (if `None`, one is created) + @param enable_settings_manager: if True, enables a SettingsManager for + this application to manage default settings and backend sync + """ super().__init__(bus=bus, gui=gui, resources_dir=resources_dir, enable_settings_manager=enable_settings_manager) self.skill_id = skill_id @@ -22,25 +39,38 @@ def __init__(self, skill_id, bus=None, resources_dir=None, bus = get_mycroft_bus() self._startup(bus, skill_id) if settings: - LOG.warning("settings arg is deprecated and will be removed " - "in a future release") + log_deprecation(f"Settings should be set in {self._settings_path}. " + f"Passing `settings` to __init__ is not supported.", + "0.1.0") self.settings.merge(settings) @property - def _settings_path(self): + def _settings_path(self) -> str: + """ + Overrides the default path to put settings in `apps` subdirectory. + """ return join(get_xdg_config_save_path(), 'apps', self.skill_id, 'settings.json') def default_shutdown(self): + """ + Shutdown this application. + """ self.clear_intents() super().default_shutdown() if self._dedicated_bus: self.bus.close() - def get_language_dir(self, base_path=None, lang=None): - """ checks for all language variations and returns best path - eg, if lang is set to pt-pt but only pt-br resources exist, - those will be loaded instead of failing, or en-gb vs en-us and so on + def get_language_dir(self, base_path: Optional[str] = None, + lang: Optional[str] = None) -> Optional[str]: + """ + Get the best matched language resource directory for the requested lang. + This will consider dialects for the requested language, i.e. if lang is + set to pt-pt but only pt-br resources exist, the `pt-br` resource path + will be returned. + @param base_path: root path to find resources (default res_dir) + @param lang: language to get resources for (default self.lang) + @return: path to language resources if they exist, else None """ base_path = base_path or self.res_dir @@ -56,13 +86,15 @@ def get_language_dir(self, base_path=None, lang=None): similar_dialect_directories = locate_lang_directories(lang, base_path) for directory in similar_dialect_directories: if directory.exists(): - return directory + return str(directory) def clear_intents(self): - # remove bus handlers, otherwise if re-registered we get multiple - # handler executions + """ + Remove bus event handlers and detach from the intent service to prevent + multiple registered handlers. + """ for intent_name, _ in self.intent_service: event_name = f'{self.skill_id}:{intent_name}' self.remove_event(event_name) - - self.intent_service.detach_all() # delete old intents before re-registering + # delete old intents before re-registering + self.intent_service.detach_all() diff --git a/test/unittests/test_abstract_app.py b/test/unittests/test_abstract_app.py index d743ce2b..38847a43 100644 --- a/test/unittests/test_abstract_app.py +++ b/test/unittests/test_abstract_app.py @@ -2,6 +2,7 @@ from os.path import join, dirname from os import remove +from unittest.mock import Mock, patch from ovos_utils.gui import GUIInterface from ovos_utils.messagebus import FakeBus @@ -84,6 +85,28 @@ def test_settings_path(self): remove(test_app._settings_path) remove(test_skill._settings_path) + @patch("ovos_workshop.app.OVOSSkill.default_shutdown") + def test_default_shutdown(self, skill_shutdown): + real_clear_intents = self.app.clear_intents + real_bus_close = self.app.bus.close + self.app.bus.close = Mock() + self.app.clear_intents = Mock() + self.app.default_shutdown() + self.app.clear_intents.assert_called_once() + self.app.bus.close.assert_called_once() + skill_shutdown.assert_called_once() + + self.app.bus.close = real_bus_close + self.app.clear_intents = real_clear_intents + + def test_get_language_dir(self): + # TODO + pass + + def test_clear_intents(self): + # TODO + pass + def test_class_inheritance(self): from ovos_workshop.skills.base import BaseSkill from ovos_workshop.skills.ovos import OVOSSkill From a053a613846af098d005aa0bc796553688d59419 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 10 Jul 2023 19:08:33 +0000 Subject: [PATCH 112/154] Increment Version to 0.0.12a37 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 19250349..bd0883cd 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 36 +VERSION_ALPHA = 37 # END_VERSION_BLOCK From 984edf0bfdfa3340d188bc79cdeffe61c32eecf2 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Mon, 10 Jul 2023 19:09:06 +0000 Subject: [PATCH 113/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8ad821..30f3afa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a36](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a36) (2023-07-10) +## [0.0.12a37](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a37) (2023-07-10) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a35...0.0.12a36) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a36...0.0.12a37) + +**Merged pull requests:** + +- Add docstrings and unit tests for app.py [\#110](https://github.com/OpenVoiceOS/OVOS-workshop/pull/110) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a36](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a36) (2023-07-10) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a35...V0.0.12a36) **Implemented enhancements:** From 0993c00b4a7bf30de0153e344cd118bca3be6731 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Wed, 12 Jul 2023 08:06:40 +0100 Subject: [PATCH 114/154] Update requirements.txt (#112) --- requirements/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5d45b42c..42f45afc 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ -ovos-utils < 0.1.0, >=0.0.35a6 +ovos-utils < 0.1.0, >=0.0.35a7 ovos_config < 0.1.0,>=0.0.5 ovos-lingua-franca~=0.4,>=0.4.6 -ovos-bus-client < 0.1.0, >=0.0.5a1 -ovos_backend_client<=0.1.0 +ovos-bus-client < 0.1.0, >=0.0.5 +ovos_backend_client>=0.1.0a6 rapidfuzz From df2106d7080705ccc588cd47e0c8617adbfe2aac Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 07:06:55 +0000 Subject: [PATCH 115/154] Increment Version to 0.0.12a38 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index bd0883cd..b20721d7 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 37 +VERSION_ALPHA = 38 # END_VERSION_BLOCK From 544b21fdad799ced972af85f64f3c2d6aae919ce Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 07:07:27 +0000 Subject: [PATCH 116/154] Update Changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30f3afa7..9433d27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a37](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a37) (2023-07-10) +## [0.0.12a38](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a38) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a36...0.0.12a37) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a37...0.0.12a38) + +**Merged pull requests:** + +- Update requirements.txt [\#112](https://github.com/OpenVoiceOS/OVOS-workshop/pull/112) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a37](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a37) (2023-07-10) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a36...V0.0.12a37) **Merged pull requests:** From 6449aa22cd342106b2d943d061d12553e2c00055 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:08:41 -0700 Subject: [PATCH 117/154] Add locking around skill settings changes (#114) --- ovos_workshop/skills/base.py | 32 ++++++++++-------- test/unittests/skills/test_base.py | 52 ++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 91d0079e..7d83050b 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -23,7 +23,7 @@ from inspect import signature from itertools import chain from os.path import join, abspath, dirname, basename, isfile -from threading import Event +from threading import Event, RLock from typing import List, Optional, Dict, Callable, Union from ovos_bus_client import MessageBusClient @@ -182,6 +182,7 @@ def __init__(self, name: Optional[str] = None, self._settings = None self._initial_settings = settings or dict() self._settings_watchdog = None + self._settings_lock = RLock() # Override to register a callback method that will be called every time # the skill's settings are updated. The referenced method should @@ -319,9 +320,10 @@ def settings(self, val: dict): if self._settings is None: self._initial_settings = val return - # ensure self._settings remains a JsonDatabase - self._settings.clear() # clear data - self._settings.merge(val, skip_empty=False) # merge new data + with self._settings_lock: + # ensure self._settings remains a JsonDatabase + self._settings.clear() # clear data + self._settings.merge(val, skip_empty=False) # merge new data # not a property in mycroft-core @property @@ -616,15 +618,16 @@ def _init_settings(self): # NOTE: lock is disabled due to usage of deepcopy and to allow json # serialization self._settings = JsonStorage(self._settings_path, disable_lock=True) - if self._initial_settings and not self._is_fully_initialized: - self.log.warning("Copying default settings values defined in " - "__init__ \nto correct this add kwargs " - "__init__(bus=None, skill_id='') " - f"to skill class {self.__class__.__name__}") - for k, v in self._initial_settings.items(): - if k not in self._settings: - self._settings[k] = v - self._initial_settings = copy(self.settings) + with self._settings_lock: + if self._initial_settings and not self._is_fully_initialized: + self.log.warning("Copying default settings values defined in " + "__init__ \nto correct this add kwargs " + "__init__(bus=None, skill_id='') " + f"to skill class {self.__class__.__name__}") + for k, v in self._initial_settings.items(): + if k not in self._settings: + self._settings[k] = v + self._initial_settings = copy(self.settings) self._start_filewatcher() @@ -677,7 +680,8 @@ def _handle_settings_file_change(self, path: str): @param path: Modified file path """ if self._settings: - self._settings.reload() + with self._settings_lock: + self._settings.reload() if self.settings_change_callback: try: self.settings_change_callback() diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index 38f19d64..06a61b41 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -1,6 +1,10 @@ +import os +import shutil import unittest from logging import Logger +from threading import Event, Thread +from time import time from unittest.mock import Mock, patch from os.path import join, dirname, isdir @@ -19,11 +23,18 @@ def test_simple_trace(self): class TestBaseSkill(unittest.TestCase): + test_config_path = join(dirname(__file__), "temp_config") + os.environ["XDG_CONFIG_HOME"] = test_config_path from ovos_workshop.skills.base import BaseSkill bus = FakeBus() skill_id = "test_base_skill" skill = BaseSkill(bus=bus, skill_id=skill_id) + @classmethod + def tearDownClass(cls) -> None: + os.environ.pop("XDG_CONFIG_HOME") + shutil.rmtree(cls.test_config_path) + def test_00_skill_init(self): from ovos_workshop.settings import SkillSettingsManager from ovos_workshop.skills.base import SkillGUI @@ -90,8 +101,45 @@ def test_startup(self): pass def test_init_settings(self): - # TODO - pass + # Test initial settings defined and not fully initialized + test_settings = {"init": True} + self.skill._initial_settings = test_settings + self.skill._settings["init"] = False + self.skill._settings["test"] = "value" + self.skill._init_event.clear() + self.skill._init_settings() + self.assertEqual(dict(self.skill.settings), + {**test_settings, + **{"__mycroft_skill_firstrun": False}}) + self.assertEqual(dict(self.skill._initial_settings), + dict(self.skill.settings)) + + # Test settings changed during init + stop_event = Event() + setting_event = Event() + + def _update_skill_settings(): + while not stop_event.is_set(): + self.skill.settings["test_val"] = time() + setting_event.set() + + # Test this a few times since this handles a race condition + for i in range(8): + setting_event.clear() + stop_event.clear() + thread = Thread(target=_update_skill_settings, daemon=True) + thread.start() + setting_event.wait() # settings have some value + self.skill._init_settings() + setting_event.clear() + setting_event.wait() # settings updated since init + stop_time = time() + stop_event.set() + thread.join() + self.assertAlmostEquals(self.skill.settings["test_val"], stop_time, + 0) + self.assertNotEqual(self.skill.settings["test_val"], + self.skill._initial_settings["test_val"]) def test_init_skill_gui(self): # TODO From f7e36edb61737dac7d454d21df9a64521723f541 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:09:00 +0000 Subject: [PATCH 118/154] Increment Version to 0.0.12a39 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index b20721d7..ca47f6bf 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 38 +VERSION_ALPHA = 39 # END_VERSION_BLOCK From 17ec30db9bceba0613462409009a0f9103041119 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:09:33 +0000 Subject: [PATCH 119/154] Update Changelog --- CHANGELOG.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9433d27d..8c539a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a38](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a38) (2023-07-12) +## [0.0.12a39](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a39) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a37...0.0.12a38) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a38...0.0.12a39) + +**Closed issues:** + +- When core restarts, I got this trace in the "hello world" skill [\#93](https://github.com/OpenVoiceOS/OVOS-workshop/issues/93) + +**Merged pull requests:** + +- Add locking around skill settings changes [\#114](https://github.com/OpenVoiceOS/OVOS-workshop/pull/114) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a38](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a38) (2023-07-12) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a37...V0.0.12a38) **Merged pull requests:** @@ -594,7 +606,6 @@ - add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) - Add new work in progress audio player ui for media service [\#6](https://github.com/OpenVoiceOS/OVOS-workshop/pull/6) ([AIIX](https://github.com/AIIX)) - Add next and previous buttons [\#4](https://github.com/OpenVoiceOS/OVOS-workshop/pull/4) ([AIIX](https://github.com/AIIX)) -- add player pos property and fix mycroft players for plugin [\#3](https://github.com/OpenVoiceOS/OVOS-workshop/pull/3) ([AIIX](https://github.com/AIIX)) **Fixed bugs:** @@ -603,8 +614,6 @@ - fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) - fix icon paths and lower version [\#7](https://github.com/OpenVoiceOS/OVOS-workshop/pull/7) ([AIIX](https://github.com/AIIX)) - fix AudioPlayer property name [\#5](https://github.com/OpenVoiceOS/OVOS-workshop/pull/5) ([AIIX](https://github.com/AIIX)) -- fix condition in video player [\#2](https://github.com/OpenVoiceOS/OVOS-workshop/pull/2) ([AIIX](https://github.com/AIIX)) -- add a timeout to videoplayer when nothing is playing for more than 60 seconds [\#1](https://github.com/OpenVoiceOS/OVOS-workshop/pull/1) ([AIIX](https://github.com/AIIX)) **Merged pull requests:** From 01f0e7aed8455a82af8f31e39b3dfc54d561deb3 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:17:19 -0700 Subject: [PATCH 120/154] Update `default_shutdown` with unit tests (#115) --- ovos_workshop/skills/base.py | 58 +++++++++++++++++++----------- test/unittests/skills/test_base.py | 41 +++++++++++++++++++-- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 7d83050b..a4d23df1 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -1974,33 +1974,49 @@ def shutdown(self): def default_shutdown(self): """ Parent function called internally to shut down everything. - - Shuts down known entities and calls skill specific shutdown method. + 1) Call skill.stop() to allow skill to clean up any active processes + 2) Store skill settings and remove file watchers + 3) Shutdown skill GUI to clear any active pages + 4) Shutdown the event_scheduler and remove any pending events + 5) Call skill.shutdown() to allow skill to do any other shutdown tasks + 6) Emit `detach_skill` Message to notify skill is shut down """ - self.settings_change_callback = None - - # Store settings - if self.settings != self._initial_settings: - self.settings.store() - if self._settings_meta: - self._settings_meta.stop() - if self._settings_watchdog: - self._settings_watchdog.shutdown() - - # Clear skill from gui - if self.gui: - self.gui.shutdown() - - # removing events - if self.event_scheduler: - self.event_scheduler.shutdown() - self.events.clear() try: + # Allow skill to handle `stop` actions before shutting things down self.stop() except Exception as e: self.log.error(f'Failed to stop skill: {self.skill_id}: {e}', exc_info=True) + + try: + self.settings_change_callback = None + + # Store settings + if self.settings != self._initial_settings: + self.settings.store() + if self._settings_meta: + self._settings_meta.stop() + if self._settings_watchdog: + self._settings_watchdog.shutdown() + except Exception as e: + self.log.error(f"Failed to store settings for {self.skill_id}: {e}") + + try: + # Clear skill from gui + if self.gui: + self.gui.shutdown() + except Exception as e: + self.log.error(f"Failed to shutdown gui for {self.skill_id}: {e}") + + try: + # removing events + if self.event_scheduler: + self.event_scheduler.shutdown() + self.events.clear() + except Exception as e: + self.log.error(f"Failed to remove events for {self.skill_id}: {e}") + try: self.shutdown() except Exception as e: @@ -2008,7 +2024,7 @@ def default_shutdown(self): f'error: {e}') self.bus.emit( - Message('detach_skill', {'skill_id': str(self.skill_id) + ':'}, + Message('detach_skill', {'skill_id': f"{self.skill_id}:"}, {"skill_id": self.skill_id})) def schedule_event(self, handler: callable, diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index 06a61b41..fc4b7735 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -386,8 +386,45 @@ def test_shutdown(self): self.skill.shutdown() def test_default_shutdown(self): - # TODO - pass + test_skill_id = "test_shutdown.skill" + test_skill = self.BaseSkill(bus=self.bus, skill_id=test_skill_id) + test_skill.settings["changed"] = True + test_skill.stop = Mock() + test_skill.shutdown = Mock() + test_skill.settings_change_callback = Mock() + test_skill.settings.store = Mock() + test_skill._settings_watchdog = Mock() + test_skill.gui.shutdown = Mock() + test_skill.event_scheduler = Mock() + test_skill.events = Mock() + message = None + + def _handle_detach_skill(msg): + nonlocal message + message = msg + + self.bus.on("detach_skill", _handle_detach_skill) + + test_skill.default_shutdown() + + test_skill.stop.assert_called_once() + + self.assertIsNone(test_skill.settings_change_callback) + test_skill.settings.store.assert_called_once() + test_skill._settings_watchdog.shutdown.assert_called_once() + + test_skill.gui.shutdown.assert_called_once() + + test_skill.event_scheduler.shutdown.assert_called_once() + test_skill.events.clear.assert_called_once() + + test_skill.shutdown.assert_called_once() + + from ovos_bus_client import Message + self.assertIsInstance(message, Message) + self.assertEqual(message.msg_type, "detach_skill") + self.assertTrue(message.data["skill_id"].startswith(test_skill_id)) + self.assertEqual(message.context["skill_id"], test_skill_id) def test_schedule_event(self): # TODO From ea72dbfe1efae6c6158e748ce6b2193ac74feeec Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:17:39 +0000 Subject: [PATCH 121/154] Increment Version to 0.0.12a40 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index ca47f6bf..f96e3ad8 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 39 +VERSION_ALPHA = 40 # END_VERSION_BLOCK From ee896516c1c03c9b0d33758e9a321030aa7de082 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:18:10 +0000 Subject: [PATCH 122/154] Update Changelog --- CHANGELOG.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c539a46..212ad0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog -## [0.0.12a39](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a39) (2023-07-12) +## [0.0.12a40](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a40) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a38...0.0.12a39) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a39...0.0.12a40) + +**Closed issues:** + +- When core restarts, I got this trace in the "wikipedia" and "weather" skills [\#94](https://github.com/OpenVoiceOS/OVOS-workshop/issues/94) + +**Merged pull requests:** + +- Update `default_shutdown` with unit tests [\#115](https://github.com/OpenVoiceOS/OVOS-workshop/pull/115) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a39](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a39) (2023-07-12) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a38...V0.0.12a39) **Closed issues:** @@ -605,7 +617,6 @@ - Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) - add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) - Add new work in progress audio player ui for media service [\#6](https://github.com/OpenVoiceOS/OVOS-workshop/pull/6) ([AIIX](https://github.com/AIIX)) -- Add next and previous buttons [\#4](https://github.com/OpenVoiceOS/OVOS-workshop/pull/4) ([AIIX](https://github.com/AIIX)) **Fixed bugs:** @@ -613,7 +624,6 @@ - remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) - fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) - fix icon paths and lower version [\#7](https://github.com/OpenVoiceOS/OVOS-workshop/pull/7) ([AIIX](https://github.com/AIIX)) -- fix AudioPlayer property name [\#5](https://github.com/OpenVoiceOS/OVOS-workshop/pull/5) ([AIIX](https://github.com/AIIX)) **Merged pull requests:** From 6547bf0852c0466c5ea932e4ac8af2af6733be36 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 12 Jul 2023 10:18:21 -0700 Subject: [PATCH 123/154] Docstrings, Annotation, and Outlined unit tests (#111) --- ovos_workshop/filesystem.py | 59 +++++----- ovos_workshop/permissions.py | 46 +++++--- ovos_workshop/resource_files.py | 157 +++++++++++++++----------- ovos_workshop/settings.py | 43 +++---- ovos_workshop/skill_launcher.py | 110 +++++++++++------- test/unittests/test_filesystem.py | 6 +- test/unittests/test_resource_files.py | 96 +++++++++++++++- test/unittests/test_settings.py | 1 - test/unittests/test_skill_launcher.py | 72 +++++++++++- 9 files changed, 403 insertions(+), 187 deletions(-) diff --git a/ovos_workshop/filesystem.py b/ovos_workshop/filesystem.py index 45a84225..05243efd 100644 --- a/ovos_workshop/filesystem.py +++ b/ovos_workshop/filesystem.py @@ -11,27 +11,32 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -# + import os import shutil from os.path import join, expanduser, isdir +from typing import TextIO + from ovos_config.locations import get_xdg_data_save_path from ovos_config.meta import get_xdg_base +from ovos_utils.log import log_deprecation class FileSystemAccess: - """A class for providing access to the mycroft FS sandbox. - - Intended to be attached to skills at initialization time to provide a - skill-specific namespace. - """ - - def __init__(self, path): - #: Member value containing the root path of the namespace + def __init__(self, path: str): + """ + Create a filesystem in a valid location. + @param path: path basename/name of the module requesting a filesystem + """ self.path = self.__init_path(path) @staticmethod - def __init_path(path): + def __init_path(path: str): + """ + Initialize a directory for filesystem access. + @param path: path basename to initialize + @return: validated existing path for this filesystem + """ if not isinstance(path, str) or len(path) == 0: raise ValueError("path must be initialized as a non empty string") @@ -39,37 +44,27 @@ def __init_path(path): xdg_path = expanduser(f'{get_xdg_data_save_path()}/filesystem/{path}') # Migrate from the old location if it still exists if isdir(old_path) and not isdir(xdg_path): + log_deprecation(f"Settings at {old_path} will be ignored", "0.1.0") shutil.move(old_path, xdg_path) if not isdir(xdg_path): os.makedirs(xdg_path) return xdg_path - def open(self, filename, mode): - """Open a file in the provided namespace. - - Get a handle to a file (with the provided mode) within the - skill-specific namespace. - - Parameters: - filename (str): a path relative to the namespace. - subdirs not currently supported. - - mode (str): a file handle mode - - Returns: - an open file handle. + def open(self, filename: str, mode: str) -> TextIO: + """ + Open the requested file in this FileSystem in the requested mode. + @param filename: string filename, relative to this FileSystemAccess + @param mode: mode to open file with (i.e. `rb`, `w+`) + @return: TextIO object for the requested file in the requested mode """ file_path = join(self.path, filename) return open(file_path, mode) - def exists(self, filename): - """Check if file exists in the namespace. - - Args: - filename (str): a path relative to the namespace. - subdirs not currently supported. - Returns: - bool: True if file exists, else False. + def exists(self, filename: str) -> bool: + """ + Check if a file exists in the namespace. + @param filename: string filename, relative to this FileSystemAccess + @return: True if the filename exists, else False """ return os.path.exists(join(self.path, filename)) diff --git a/ovos_workshop/permissions.py b/ovos_workshop/permissions.py index c4c59431..9325439d 100644 --- a/ovos_workshop/permissions.py +++ b/ovos_workshop/permissions.py @@ -1,26 +1,40 @@ -""" -mode of operation is defined in the .conf file for the different components -""" import enum class ConverseMode(str, enum.Enum): - ACCEPT_ALL = "accept_all" # default mycroft-core behavior - WHITELIST = "whitelist" # only call converse for skills in whitelist - BLACKLIST = "blacklist" # only call converse for skills NOT in blacklist + """ + Defines a mode for handling `converse` requests. + ACCEPT ALL - default behavior where all skills may implement `converse` + WHITELIST - only skills explicitly allowed may implement `converse` + BLACKLIST - all skills except those disallowed may implement `converse` + """ + ACCEPT_ALL = "accept_all" # Default + WHITELIST = "whitelist" + BLACKLIST = "blacklist" class FallbackMode(str, enum.Enum): - ACCEPT_ALL = "accept_all" # default mycroft-core behavior - WHITELIST = "whitelist" # only call fallback for skills in whitelist - BLACKLIST = "blacklist" # only call fallback for skills NOT in blacklist + """ + Defines a mode for handling fallbacks (utterances without a matched intent) + ACCEPT ALL - default behavior where all installed FallbackSkills are used + WHITELIST - only explicitly allowed FallbackSkills may respond + BLACKLIST - all FallbackSkills except those disallowed may respond + """ + ACCEPT_ALL = "accept_all" # Default + WHITELIST = "whitelist" + BLACKLIST = "blacklist" class ConverseActivationMode(str, enum.Enum): - ACCEPT_ALL = "accept_all" # default mycroft-core behavior - PRIORITY = "priority" # skills can only activate themselves if no skill - # with higher priority is active - WHITELIST = "whitelist" # only skills in "converse_whitelist" - # can activate themselves - BLACKLIST = "blacklist" # only skills NOT in converse "converse_blacklist" - # can activate themselves + """ + Defines a mode for manually activating `converse` handling + ACCEPT ALL - default behavior where any skill may activate itself + PRIORITY - a skill may only activate itself if no higher-priority skill is + currently active + WHITELIST - only explicitly allowed skills may activate themselves + BLACKLIST - all skills except those disallowed may activate themselves + """ + ACCEPT_ALL = "accept_all" # Default + PRIORITY = "priority" + WHITELIST = "whitelist" + BLACKLIST = "blacklist" diff --git a/ovos_workshop/resource_files.py b/ovos_workshop/resource_files.py index 5ecd1b83..21aa5df5 100644 --- a/ovos_workshop/resource_files.py +++ b/ovos_workshop/resource_files.py @@ -179,7 +179,8 @@ class ResourceType: base_directory: directory containing all files for the resource type """ - def __init__(self, resource_type: str, file_extension: str, language=None): + def __init__(self, resource_type: str, file_extension: str, + language: Optional[str] = None): self.resource_type = resource_type self.file_extension = file_extension self.language = language @@ -215,7 +216,7 @@ def _locate_base_no_lang(self, skill_directory, resource_subdirectory): if self.user_directory: self.base_directory = self.user_directory - def locate_base_directory(self, skill_directory): + def locate_base_directory(self, skill_directory: str) -> Optional[str]: """Find the skill's base directory for the specified resource type. There are three supported methodologies for storing resource files. @@ -301,7 +302,7 @@ def __init__(self, resource_type, resource_name): self.resource_name = resource_name self.file_path = self._locate() - def _locate(self): + def _locate(self) -> str: """Locates a resource file in the skill's locale directory. A skill's locale directory can contain a subdirectory structure defined @@ -530,7 +531,10 @@ def load(self) -> Optional[str]: class SkillResources: - def __init__(self, skill_directory, language, dialog_renderer=None, skill_id=None): + def __init__(self, skill_directory: str, + language: str, + dialog_renderer: Optional[MustacheDialogRenderer] = None, + skill_id: Optional[str] = None): self.skill_directory = skill_directory self.language = language self.skill_id = skill_id @@ -539,16 +543,25 @@ def __init__(self, skill_directory, language, dialog_renderer=None, skill_id=Non self.static = dict() @property - def dialog_renderer(self): + def dialog_renderer(self) -> MustacheDialogRenderer: + """ + Get a dialog renderer object for these resources + """ if not self._dialog_renderer: self._load_dialog_renderer() return self._dialog_renderer @dialog_renderer.setter - def dialog_renderer(self, val): + def dialog_renderer(self, val: MustacheDialogRenderer): + """ + Set the dialog renderer object for these resources + """ self._dialog_renderer = val def _load_dialog_renderer(self): + """ + Initialize a MustacheDialogRenderer object for these resources + """ base_dirs = locate_lang_directories(self.language, self.skill_directory, "dialog") @@ -560,7 +573,8 @@ def _load_dialog_renderer(self): LOG.debug(f'No dialog loaded for {self.language}') def _define_resource_types(self) -> SkillResourceTypes: - """Defines all known types of skill resource files. + """ + Defines all known types of skill resource files. A resource file contains information the skill needs to function. Examples include dialog files to be spoken and vocab files for intent @@ -584,8 +598,10 @@ def _define_resource_types(self) -> SkillResourceTypes: resource_type.locate_base_directory(self.skill_directory) return SkillResourceTypes(**resource_types) - def load_dialog_file(self, name, data=None) -> List[str]: - """Loads the contents of a dialog file into memory. + def load_dialog_file(self, name: str, + data: Optional[dict] = None) -> List[str]: + """ + Loads the contents of a dialog file into memory. Named variables in the dialog are populated with values found in the data dictionary. @@ -600,12 +616,14 @@ def load_dialog_file(self, name, data=None) -> List[str]: dialog_file.data = data return dialog_file.load() - def locate_qml_file(self, name): + def locate_qml_file(self, name: str) -> str: qml_file = QmlFile(self.types.qml, name) return qml_file.load() - def load_list_file(self, name, data=None) -> List[str]: - """Load a file containing a list of words or phrases + def load_list_file(self, name: str, + data: Optional[dict] = None) -> List[str]: + """ + Load a file containing a list of words or phrases Named variables in the dialog are populated with values found in the data dictionary. @@ -620,8 +638,10 @@ def load_list_file(self, name, data=None) -> List[str]: list_file.data = data return list_file.load() - def load_named_value_file(self, name, delimiter=None) -> dict: - """Load file containing a set names and values. + def load_named_value_file(self, name: str, + delimiter: Optional[str] = None) -> dict: + """ + Load file containing a set names and values. Loads a simple delimited file of name/value pairs. The name is the first item, the value is the second. @@ -643,8 +663,9 @@ def load_named_value_file(self, name, delimiter=None) -> dict: return named_values - def load_regex_file(self, name) -> List[str]: - """Loads a file containing regular expression patterns. + def load_regex_file(self, name: str) -> List[str]: + """ + Loads a file containing regular expression patterns. The regular expression patterns are generally used to find a value in a user utterance the skill needs to properly perform the requested @@ -658,8 +679,10 @@ def load_regex_file(self, name) -> List[str]: regex_file = RegexFile(self.types.regex, name) return regex_file.load() - def load_template_file(self, name, data=None) -> List[str]: - """Loads the contents of a dialog file into memory. + def load_template_file(self, name: str, + data: Optional[dict] = None) -> List[str]: + """ + Loads the contents of a dialog file into memory. Named variables in the dialog are populated with values found in the data dictionary. @@ -674,12 +697,13 @@ def load_template_file(self, name, data=None) -> List[str]: template_file.data = data return template_file.load() - def load_vocabulary_file(self, name) -> List[List[str]]: - """Loads a file containing variations of words meaning the same thing. + def load_vocabulary_file(self, name: str) -> List[List[str]]: + """ + Loads a file containing variations of words meaning the same thing. A vocabulary file defines words a skill uses for intent matching. It can also be used to match words in an utterance after intent - intent matching is complete. + matching is complete. Args: name: name of the regular expression file, no extension needed @@ -689,7 +713,7 @@ def load_vocabulary_file(self, name) -> List[List[str]]: vocabulary_file = VocabularyFile(self.types.vocabulary, name) return vocabulary_file.load() - def load_word_file(self, name) -> Optional[str]: + def load_word_file(self, name: str) -> Optional[str]: """Loads a file containing a word. Args: @@ -700,8 +724,9 @@ def load_word_file(self, name) -> Optional[str]: word_file = WordFile(self.types.word, name) return word_file.load() - def render_dialog(self, name, data=None) -> str: - """Selects a record from a dialog file at random for TTS purposes. + def render_dialog(self, name: str, data: Optional[dict] = None) -> str: + """ + Selects a record from a dialog file at random for TTS purposes. Args: name: name of the list file (no extension needed) @@ -714,12 +739,18 @@ def render_dialog(self, name, data=None) -> str: return resource_file.render(self.dialog_renderer) def load_skill_vocabulary(self, alphanumeric_skill_id: str) -> dict: + """ + Load all vocabulary files in the skill's resources + @param alphanumeric_skill_id: alphanumeric ID of the skill associated + with these resources. + @return: dict of vocab name to loaded contents + """ skill_vocabulary = {} base_directory = self.types.vocabulary.base_directory for directory, _, files in walk(base_directory): - vocabulary_files = [ + vocabulary_files = ( file_name for file_name in files if file_name.endswith(".voc") - ] + ) for file_name in vocabulary_files: vocab_type = alphanumeric_skill_id + file_name[:-4].title() vocabulary = self.load_vocabulary_file(file_name) @@ -729,12 +760,18 @@ def load_skill_vocabulary(self, alphanumeric_skill_id: str) -> dict: return skill_vocabulary def load_skill_regex(self, alphanumeric_skill_id: str) -> List[str]: + """ + Load all regex files in the skill's resources + @param alphanumeric_skill_id: alphanumeric ID of the skill associated + with these resources. + @return: list of string regex expressions + """ skill_regexes = [] base_directory = self.types.regex.base_directory for directory, _, files in walk(base_directory): - regex_files = [ + regex_files = ( file_name for file_name in files if file_name.endswith(".rx") - ] + ) for file_name in regex_files: skill_regexes.extend(self.load_regex_file(file_name)) @@ -745,9 +782,8 @@ def load_skill_regex(self, alphanumeric_skill_id: str) -> List[str]: return skill_regexes @staticmethod - def _make_unique_regex_group( - regexes: List[str], alphanumeric_skill_id: str - ) -> List[str]: + def _make_unique_regex_group(regexes: List[str], + alphanumeric_skill_id: str) -> List[str]: """Adds skill ID to group ID in a regular expression for uniqueness. Args: @@ -785,25 +821,21 @@ def __init__(self, language, skill_id): class RegexExtractor: - """Extracts data from an utterance using regular expressions. - - Attributes: - group_name: - regex_patterns: regular expressions read from a .rx file - """ - - def __init__(self, group_name, regex_patterns): + def __init__(self, group_name: str, regex_patterns: List[str]): + """ + Init an object representing an entity and a list of possible regex + patterns for extracting it + @param group_name: Named group entity to extract + @param regex_patterns: List of string regex patterns to evaluate + """ self.group_name = group_name self.regex_patterns = regex_patterns - def extract(self, utterance) -> Optional[str]: - """Attempt to find a value in a user request. - - Args: - utterance: request spoken by the user - - Returns: - The value extracted from the utterance, if found + def extract(self, utterance: str) -> Optional[str]: + """ + Attempt to extract `group_name` from the specified `utterance` + @param utterance: String to evaluate + @return: Extracted `group_name` value if matched in `utterance` """ extract = None pattern_match = self._match_utterance_to_patterns(utterance) @@ -813,14 +845,12 @@ def extract(self, utterance) -> Optional[str]: return extract - def _match_utterance_to_patterns(self, utterance: str): - """Match regular expressions to user request. - - Args: - utterance: request spoken by the user - - Returns: - a regular expression match object if a match is found + def _match_utterance_to_patterns(self, + utterance: str) -> Optional[re.Match]: + """ + Compare `utterance` to all `regex_patterns` until a match is found. + @param utterance: String to evaluate + @return: re.Match object if utterance mathes any `regex_patterns` """ pattern_match = None for pattern in self.regex_patterns: @@ -830,11 +860,11 @@ def _match_utterance_to_patterns(self, utterance: str): return pattern_match - def _extract_group_from_match(self, pattern_match): - """Extract the alarm name from the utterance. - - Args: - pattern_match: a regular expression match object + def _extract_group_from_match(self, pattern_match: re.Match) -> str: + """ + Extract the specified regex group value. + @param pattern_match: Match object associated with a particular input + @return: String matched to `self.group_name` """ extract = None try: @@ -848,10 +878,9 @@ def _extract_group_from_match(self, pattern_match): return extract def _log_extraction_result(self, extract: str): - """Log the results of the matching. - - Args: - extract: the value extracted from the user utterance + """ + Log the results of the matching. + @param extract: the value extracted from the user utterance """ if extract is None: LOG.info(f"No {self.group_name.lower()} extracted from utterance") diff --git a/ovos_workshop/settings.py b/ovos_workshop/settings.py index 4bb9617f..1e7a4a39 100644 --- a/ovos_workshop/settings.py +++ b/ovos_workshop/settings.py @@ -1,24 +1,27 @@ import json +import yaml + from os.path import isfile +from typing import Optional from threading import Timer - -import yaml from ovos_backend_client.api import DeviceApi from ovos_backend_client.pairing import is_paired, requires_backend from ovos_backend_client.settings import RemoteSkillSettings, get_display_name +from ovos_bus_client import MessageBusClient from ovos_bus_client.message import Message, dig_for_message from ovos_utils.log import LOG class SkillSettingsManager: def __init__(self, skill): - self.download_timer = None - self.skill = skill + from ovos_workshop.skills.base import BaseSkill + self.download_timer: Optional[Timer] = None + self.skill: BaseSkill = skill self.api = DeviceApi() - self.remote_settings = RemoteSkillSettings(self.skill_id, - settings=dict(self.skill.settings), - meta=self.load_meta(), - remote_id=self.skill_gid) + self.remote_settings = \ + RemoteSkillSettings(self.skill_id, + settings=dict(self.skill.settings), + meta=self.load_meta(), remote_id=self.skill_gid) self.register_bus_handlers() def start(self): @@ -44,23 +47,23 @@ def stop(self): self.download_timer.cancel() @property - def bus(self): + def bus(self) -> MessageBusClient: return self.skill.bus @property - def skill_id(self): + def skill_id(self) -> str: return self.skill.skill_id @property - def display_name(self): + def display_name(self) -> str: return get_display_name(self.skill_id) @property - def skill_gid(self): + def skill_gid(self) -> str: return f"@{self.api.uuid}|{self.skill_id}" @property - def skill_meta(self): + def skill_meta(self) -> dict: return self.remote_settings.meta def register_bus_handlers(self): @@ -75,7 +78,7 @@ def register_bus_handlers(self): self.skill.add_event('mycroft.paired', self.handle_upload_local) - def load_meta(self): + def load_meta(self) -> dict: json_path = f"{self.skill.root_dir}/settingsmeta.json" yaml_path = f"{self.skill.root_dir}/settingsmeta.yaml" if isfile(yaml_path): @@ -86,7 +89,7 @@ def load_meta(self): return json.load(meta_file) return {} - def save_meta(self, generate=False): + def save_meta(self, generate: bool = False): # unset reload flag to avoid a reload on settingmeta change # TODO - support for settingsmeta XDG paths reload = self.skill.reload_skill @@ -110,7 +113,7 @@ def save_meta(self, generate=False): self.skill.reload_skill = reload @requires_backend - def upload(self, generate=False): + def upload(self, generate: bool = False): if not is_paired(): LOG.error("Device needs to be paired to upload settings") return @@ -120,7 +123,7 @@ def upload(self, generate=False): self.remote_settings.upload() @requires_backend - def upload_meta(self, generate=False): + def upload_meta(self, generate: bool = False): if not is_paired(): LOG.error("Device needs to be paired to upload settingsmeta") return @@ -145,15 +148,15 @@ def download(self): msg.data[self.skill_id] = self.remote_settings.settings self.bus.emit(msg) - def handle_upload_meta(self, message): + def handle_upload_meta(self, message: Message): skill_id = message.data.get("skill_id") if skill_id == self.skill_id: self.upload_meta() - def handle_upload_local(self, message): + def handle_upload_local(self, message: Message): skill_id = message.data.get("skill_id") if skill_id == self.skill_id: self.upload() - def handle_download_remote(self, message): + def handle_download_remote(self, message: Message): self.download() diff --git a/ovos_workshop/skill_launcher.py b/ovos_workshop/skill_launcher.py index ae9cffe5..e5ca41ab 100644 --- a/ovos_workshop/skill_launcher.py +++ b/ovos_workshop/skill_launcher.py @@ -1,14 +1,11 @@ import gc -import importlib import os -from os.path import isdir import sys +from os.path import isdir from inspect import isclass from types import ModuleType from typing import Optional - from time import time - from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_config.config import Configuration @@ -16,7 +13,7 @@ from ovos_plugin_manager.skills import find_skill_plugins from ovos_utils import wait_for_exit_signal from ovos_utils.file_utils import FileWatcher -from ovos_utils.log import LOG +from ovos_utils.log import LOG, deprecated, log_deprecation from ovos_utils.process_utils import RuntimeRequirements from ovos_utils.skills.locations import get_skill_directories as _get_skill_dirs @@ -38,25 +35,22 @@ SKILL_MAIN_MODULE = '__init__.py' +@deprecated("This method has moved to `ovos_utils.skills.locations`", "0.1.0") def get_skill_directories(conf=None): - # TODO: Deprecate in 0.1.0 - LOG.warning(f"This method has moved to `ovos_utils.skills.locations` " - f"and will be removed in a future release.") conf = conf or Configuration() return _get_skill_dirs(conf) +@deprecated("This method has moved to `ovos_utils.skills.locations`", "0.1.0") def get_default_skills_directory(conf=None): - # TODO: Deprecate in 0.1.0 - LOG.warning(f"This method has moved to `ovos_utils.skills.locations` " - f"and will be removed in a future release.") from ovos_utils.skills.locations import get_default_skills_directory conf = conf or Configuration() return get_default_skills_directory(conf) def remove_submodule_refs(module_name: str): - """Ensure submodules are reloaded by removing the refs from sys.modules. + """ + Ensure submodules are reloaded by removing the refs from sys.modules. Python import system puts a reference for each module in the sys.modules dictionary to bypass loading if a module is already in memory. To make @@ -78,7 +72,8 @@ def remove_submodule_refs(module_name: str): def load_skill_module(path: str, skill_id: str) -> ModuleType: - """Load a skill module + """ + Load a skill module This function handles the differences between python 3.4 and 3.5+ as well as makes sure the module is inserted into the sys.modules dict. @@ -89,6 +84,7 @@ def load_skill_module(path: str, skill_id: str) -> ModuleType: Returns: loaded skill module """ + import importlib.util module_name = skill_id.replace('.', '_') remove_submodule_refs(module_name) @@ -101,7 +97,8 @@ def load_skill_module(path: str, skill_id: str) -> ModuleType: def get_skill_class(skill_module: ModuleType) -> Optional[callable]: - """Find MycroftSkill based class in skill module. + """ + Find MycroftSkill based class in skill module. Arguments: skill_module (module): module to search for Skill class @@ -138,7 +135,7 @@ def get_skill_class(skill_module: ModuleType) -> Optional[callable]: return None -def get_create_skill_function(skill_module) -> Optional[callable]: +def get_create_skill_function(skill_module: ModuleType) -> Optional[callable]: """Find create_skill function in skill module. Arguments: @@ -149,12 +146,22 @@ def get_create_skill_function(skill_module) -> Optional[callable]: """ if hasattr(skill_module, "create_skill") and \ callable(skill_module.create_skill): + log_deprecation("`create_skill` method is no longer supported", "0.1.0") return skill_module.create_skill return None class SkillLoader: - def __init__(self, bus, skill_directory=None, skill_id=None): + def __init__(self, bus: MessageBusClient, + skill_directory: Optional[str] = None, + skill_id: Optional[str] = None): + """ + Create a SkillLoader object to load/unload a skill and + @param bus: MessageBusClient object + @param skill_directory: path to skill source + (containing __init__.py, locale, gui, etc.) + @param skill_id: Unique ID for the skill + """ self.bus = bus self._skill_directory = skill_directory self._skill_id = skill_id @@ -162,7 +169,7 @@ def __init__(self, bus, skill_directory=None, skill_id=None): self._loaded = None self.load_attempted = False self.last_loaded = 0 - self.instance: BaseSkill = None + self.instance: Optional[BaseSkill] = None self.active = True self._watchdog = None self.config = Configuration() @@ -178,7 +185,7 @@ def loaded(self) -> bool: @loaded.setter def loaded(self, val: bool): """ - Set the skill as loaded + Set the skill loaded state """ self._loaded = val @@ -195,7 +202,7 @@ def skill_directory(self) -> Optional[str]: @skill_directory.setter def skill_directory(self, val: str): """ - Set (override) the skill ID + Set (override) the skill directory """ self._skill_directory = val @@ -328,9 +335,9 @@ def _execute_instance_shutdown(self): """ try: self.instance.default_shutdown() - except Exception: + except Exception as e: LOG.exception(f'An error occurred while shutting down ' - f'{self.skill_id}') + f'{self.skill_id}: {e}') else: LOG.info(f'Skill {self.skill_id} shut down successfully') del self.instance @@ -384,7 +391,7 @@ def _start_filewatcher(self): callback=self._handle_filechange, recursive=True) - def _handle_filechange(self, path): + def _handle_filechange(self, path: str): """ Handle a file change notification by reloading the skill """ @@ -392,9 +399,9 @@ def _handle_filechange(self, path): try: if self.reload_allowed: self.reload() - except Exception: + except Exception as e: LOG.exception(f'Unhandled exception occurred while reloading ' - f'{self.skill_directory}') + f'{self.skill_directory}: {e}') def _prepare_for_load(self): """ @@ -474,6 +481,9 @@ def _create_skill_instance(self, # skill_id and bus kwargs. # these skills only have skill_id and bus available in initialize, # not in __init__ + log_deprecation("This initialization is deprecated. Update skill to" + "handle passed `skill_id` and `bus` kwargs", + "0.1.0") if not self.instance._is_fully_initialized: self.instance._startup(self.bus, self.skill_id) except Exception as e: @@ -539,8 +549,17 @@ def _load(self): class SkillContainer: - def __init__(self, skill_id, skill_directory=None, bus=None): - setup_locale() # ensure any initializations and resource loading is handled + def __init__(self, skill_id: str, skill_directory: Optional[str] = None, + bus: Optional[MessageBusClient] = None): + """ + Init a SkillContainer. + @param skill_id: Unique ID of the skill being loaded + @param skill_directory: path to skill source (if None, directory will be + located by `skill_id`) + @param bus: MessageBusClient object to connect (else one is created) + """ + # ensure any initializations and resource loading is handled + setup_locale() self.bus = bus self.skill_id = skill_id if not skill_directory: # preference to local skills instead of plugins @@ -553,25 +572,32 @@ def __init__(self, skill_id, skill_directory=None, bus=None): self.skill_loader = None def _connect_to_core(self): - + """ + Initialize messagebus connection and register event to load skill once + core reports ready. + """ if not self.bus: self.bus = MessageBusClient() self.bus.run_in_thread() self.bus.connected_event.wait() LOG.debug("checking skills service status") - response = self.bus.wait_for_response(Message(f'mycroft.skills.is_ready', - context={"source": "workshop", - "destination": "skills"})) + response = self.bus.wait_for_response( + Message(f'mycroft.skills.is_ready', + context={"source": "workshop", "destination": "skills"})) if response and response.data['status']: LOG.info("connected to core") self.load_skill() else: - LOG.warning("ovos-core does not seem to be running") + LOG.warning("Skills service not ready yet. Load on ready event.") self.bus.on("mycroft.ready", self.load_skill) - def load_skill(self, message=None): + def load_skill(self, message: Optional[Message] = None): + """ + Load the skill associated with this SkillContainer instance. + @param message: Message triggering skill load if available + """ if self.skill_loader: LOG.info("detected core reload, reloading skill") self.skill_loader.reload() @@ -583,6 +609,9 @@ def load_skill(self, message=None): self._launch_standalone_skill() def run(self): + """ + Connect to core and run until KeyboardInterrupt. + """ self._connect_to_core() try: wait_for_exit_signal() @@ -592,8 +621,9 @@ def run(self): self.skill_loader.deactivate() def _launch_plugin_skill(self): - """ run a plugin skill standalone """ - + """ + Launch a skill plugin associated with this SkillContainer instance. + """ plugins = find_skill_plugins() if self.skill_id not in plugins: raise ValueError(f"unknown skill_id: {self.skill_id}") @@ -601,17 +631,19 @@ def _launch_plugin_skill(self): self.skill_loader = PluginSkillLoader(self.bus, self.skill_id) try: self.skill_loader.load(skill_plugin) - except Exception: - LOG.exception(f'Load of skill {self.skill_id} failed!') + except Exception as e: + LOG.exception(f'Load of skill {self.skill_id} failed! {e}') def _launch_standalone_skill(self): - """ run a skill standalone from a directory """ + """ + Launch a local skill associated with this SkillContainer instance. + """ self.skill_loader = SkillLoader(self.bus, self.skill_directory, skill_id=self.skill_id) try: self.skill_loader.load() - except Exception: - LOG.exception(f'Load of skill {self.skill_directory} failed!') + except Exception as e: + LOG.exception(f'Load of skill {self.skill_directory} failed! {e}') def _launch_script(): diff --git a/test/unittests/test_filesystem.py b/test/unittests/test_filesystem.py index dbe4d971..d91a8278 100644 --- a/test/unittests/test_filesystem.py +++ b/test/unittests/test_filesystem.py @@ -15,10 +15,8 @@ def setUpClass(cls) -> None: @classmethod def tearDownClass(cls) -> None: data_path = environ.pop('XDG_DATA_HOME') - try: + if isdir(data_path): shutil.rmtree(data_path) - except: - pass def test_filesystem(self): fs = FileSystemAccess("test") @@ -37,4 +35,4 @@ def test_filesystem(self): file = fs.open("test.txt", "w+") self.assertIsNotNone(file) file.close() - self.assertTrue(fs.exists("test.txt")) \ No newline at end of file + self.assertTrue(fs.exists("test.txt")) diff --git a/test/unittests/test_resource_files.py b/test/unittests/test_resource_files.py index 815e88e7..fefb7339 100644 --- a/test/unittests/test_resource_files.py +++ b/test/unittests/test_resource_files.py @@ -5,7 +5,7 @@ from os.path import isdir, join, dirname -class TestResourceFileMethods(unittest.TestCase): +class TestResourceFiles(unittest.TestCase): def test_locate_base_directories(self): from ovos_workshop.resource_files import locate_base_directories # TODO @@ -35,18 +35,22 @@ def test_resource_file(self): def test_qml_file(self): from ovos_workshop.resource_files import QmlFile, ResourceFile self.assertTrue(issubclass(QmlFile, ResourceFile)) + # TODO: test locate/load def test_dialog_file(self): from ovos_workshop.resource_files import DialogFile, ResourceFile self.assertTrue(issubclass(DialogFile, ResourceFile)) + # TODO: test load/render def test_vocab_file(self): from ovos_workshop.resource_files import VocabularyFile, ResourceFile self.assertTrue(issubclass(VocabularyFile, ResourceFile)) + # TODO test load def test_named_value_file(self): from ovos_workshop.resource_files import NamedValueFile, ResourceFile self.assertTrue(issubclass(NamedValueFile, ResourceFile)) + # TODO test load/_load_line def test_list_file(self): from ovos_workshop.resource_files import ListFile, ResourceFile @@ -59,13 +63,16 @@ def test_template_file(self): def test_regex_file(self): from ovos_workshop.resource_files import RegexFile, ResourceFile self.assertTrue(issubclass(RegexFile, ResourceFile)) + # TODO: Test load def test_word_file(self): from ovos_workshop.resource_files import WordFile, ResourceFile self.assertTrue(issubclass(WordFile, ResourceFile)) + # TODO: Test load class TestSkillResources(unittest.TestCase): + from ovos_workshop.resource_files import SkillResources test_data_path = join(dirname(__file__), "xdg_data") @classmethod @@ -75,14 +82,77 @@ def setUpClass(cls) -> None: @classmethod def tearDownClass(cls) -> None: data_path = environ.pop('XDG_DATA_HOME') - try: + if isdir(data_path): shutil.rmtree(data_path) - except: - pass - def test_skill_resources(self): - from ovos_workshop.resource_files import SkillResources + def test_load_dialog_renderer(self): # TODO + pass + + def test_define_resource_types(self): + # TODO + pass + + def test_load_dialog_file(self): + # TODO + pass + + def test_locate_qml_file(self): + # TODO + pass + + def test_load_list_file(self): + # TODO + pass + + def test_load_named_value_file(self): + # TODO + pass + + def test_load_regex_file(self): + # TODO + pass + + def test_load_template_file(self): + # TODO + pass + + def test_load_vocabulary_file(self): + # TODO + pass + + def test_load_word_file(self): + # TODO + pass + + def test_render_dialog(self): + # TODO + pass + + def test_load_skill_vocabulary(self): + # TODO + pass + + def test_load_skill_regex(self): + # TODO + pass + + def test_make_unique_regex_group(self): + # TODO + pass + + +class TestCoreResources(unittest.TestCase): + test_data_path = join(dirname(__file__), "xdg_data") + + @classmethod + def setUpClass(cls) -> None: + environ['XDG_DATA_HOME'] = cls.test_data_path + @classmethod + def tearDownClass(cls) -> None: + data_path = environ.pop('XDG_DATA_HOME') + if isdir(data_path): + shutil.rmtree(data_path) def test_core_resources(self): from ovos_workshop.resource_files import CoreResources, SkillResources @@ -91,6 +161,20 @@ def test_core_resources(self): self.assertEqual(core_res.language, "en-us") self.assertTrue(isdir(core_res.skill_directory)) + +class TestUserResources(unittest.TestCase): + test_data_path = join(dirname(__file__), "xdg_data") + + @classmethod + def setUpClass(cls) -> None: + environ['XDG_DATA_HOME'] = cls.test_data_path + + @classmethod + def tearDownClass(cls) -> None: + data_path = environ.pop('XDG_DATA_HOME') + if isdir(data_path): + shutil.rmtree(data_path) + def test_user_resources(self): from ovos_workshop.resource_files import UserResources, SkillResources user_res = UserResources("en-us", "test.skill") diff --git a/test/unittests/test_settings.py b/test/unittests/test_settings.py index c917902a..e27173bb 100644 --- a/test/unittests/test_settings.py +++ b/test/unittests/test_settings.py @@ -1,5 +1,4 @@ import unittest -from unittest.mock import Mock class TestSettings(unittest.TestCase): diff --git a/test/unittests/test_skill_launcher.py b/test/unittests/test_skill_launcher.py index 187d113c..e69280e9 100644 --- a/test/unittests/test_skill_launcher.py +++ b/test/unittests/test_skill_launcher.py @@ -18,10 +18,8 @@ def setUpClass(cls) -> None: @classmethod def tearDownClass(cls) -> None: data_path = environ.pop('XDG_DATA_HOME') - try: + if isdir(data_path): shutil.rmtree(data_path) - except: - pass def test_get_skill_directories(self): from ovos_workshop.skill_launcher import get_skill_directories @@ -115,10 +113,74 @@ def test_skill_loader_init(self): self.assertFalse(loader.is_blacklisted) self.assertTrue(loader.reload_allowed) - def test_skill_loader_load_skill(self): + def test_skill_loader_reload(self): from ovos_workshop.skill_launcher import SkillLoader # TODO + def test_skill_loader_load(self): + from ovos_workshop.skill_launcher import SkillLoader + # TODO + + def test__unload(self): + # TODO + pass + + def test_unload(self): + # TODO + pass + + def test_activate(self): + # TODO + pass + + def test_deactivate(self): + # TODO + pass + + def test_execute_instance_shutdown(self): + # TODO + pass + + def test_garbage_collect(self): + # TODO + pass + + def test_emit_skill_shutdown_event(self): + # TODO + pass + + def test__load(self): + # TODO + pass + + def test_start_filewatcher(self): + # TODO + pass + + def test_handle_filechange(self): + # TODO + pass + + def test_prepare_for_load(self): + # TODO + pass + + def test_skip_load(self): + # TODO + pass + + def test_load_skill_source(self): + # TODO + pass + + def test_create_skill_instance(self): + # TODO + pass + + def test_communicate_load_status(self): + # TODO + pass + class TestPluginSkillLoader(unittest.TestCase): bus = FakeBus() @@ -131,7 +193,7 @@ def test_plugin_skill_loader_init(self): self.assertEqual(loader.bus, self.bus) self.assertEqual(loader.skill_id, "test_skill.test") - def test_plugin_skill_loader_load_skill(self): + def test_plugin_skill_loader_load(self): from ovos_workshop.skill_launcher import PluginSkillLoader # TODO From 2071de874cab35369ac9b83c27a78338cca3ef72 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:18:39 +0000 Subject: [PATCH 124/154] Increment Version to 0.0.12a41 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index f96e3ad8..c8fe91ad 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 40 +VERSION_ALPHA = 41 # END_VERSION_BLOCK From e39637cc23fe16fbb86d3a806be3ac047d0a5960 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Wed, 12 Jul 2023 17:19:13 +0000 Subject: [PATCH 125/154] Update Changelog --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 212ad0d8..02f60db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [0.0.12a40](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a40) (2023-07-12) +## [V0.0.12a40](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a40) (2023-07-12) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a39...0.0.12a40) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a39...V0.0.12a40) **Closed issues:** @@ -11,6 +11,7 @@ **Merged pull requests:** - Update `default_shutdown` with unit tests [\#115](https://github.com/OpenVoiceOS/OVOS-workshop/pull/115) ([NeonDaniel](https://github.com/NeonDaniel)) +- Docstrings, Annotation, and Outlined unit tests [\#111](https://github.com/OpenVoiceOS/OVOS-workshop/pull/111) ([NeonDaniel](https://github.com/NeonDaniel)) ## [V0.0.12a39](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a39) (2023-07-12) @@ -616,7 +617,6 @@ - add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) - Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) - add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) -- Add new work in progress audio player ui for media service [\#6](https://github.com/OpenVoiceOS/OVOS-workshop/pull/6) ([AIIX](https://github.com/AIIX)) **Fixed bugs:** From 1a3245c844fbf288c1a8a68b2340a79e050fdcfe Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 13 Jul 2023 11:09:30 -0700 Subject: [PATCH 126/154] Unit tests and improvements to settings change callback (#116) --- ovos_workshop/skills/base.py | 3 ++ test/unittests/skills/test_base.py | 57 ++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index a4d23df1..1aef96bc 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -679,6 +679,9 @@ def _handle_settings_file_change(self, path: str): changes if a backend is configured. @param path: Modified file path """ + if path != self._settings.path: + LOG.debug(f"Ignoring non-settings change") + return if self._settings: with self._settings_lock: self._settings.reload() diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index fc4b7735..c56701de 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -1,3 +1,4 @@ +import json import os import shutil import unittest @@ -124,22 +125,31 @@ def _update_skill_settings(): setting_event.set() # Test this a few times since this handles a race condition - for i in range(8): + for i in range(32): + # Reset to pre-initialized state + self.skill._init_event.clear() + self.skill._settings = None setting_event.clear() stop_event.clear() thread = Thread(target=_update_skill_settings, daemon=True) thread.start() setting_event.wait() # settings have some value + self.assertIsNotNone(self.skill._initial_settings["test_val"], + f"run {i}") self.skill._init_settings() + self.assertIsNotNone(self.skill.settings["test_val"], f"run {i}") + self.assertIsNotNone(self.skill._initial_settings["test_val"], + f"run {i}") setting_event.clear() setting_event.wait() # settings updated since init stop_time = time() stop_event.set() thread.join() self.assertAlmostEquals(self.skill.settings["test_val"], stop_time, - 0) + 0, f"run {i}") self.assertNotEqual(self.skill.settings["test_val"], - self.skill._initial_settings["test_val"]) + self.skill._initial_settings["test_val"], + f"run {i}") def test_init_skill_gui(self): # TODO @@ -150,16 +160,49 @@ def test_init_settings_manager(self): pass def test_start_filewatcher(self): - # TODO - pass + test_skill_id = "test_settingschanged.skill" + test_skill = self.BaseSkill(bus=self.bus, skill_id=test_skill_id) + settings_changed = Event() + on_file_change = Mock(side_effect=lambda x: settings_changed.set()) + test_skill._handle_settings_file_change = on_file_change + test_skill._settings_watchdog = None + test_skill._start_filewatcher() + self.assertIsNotNone(test_skill._settings_watchdog) + skill_settings = test_skill.settings + skill_settings["changed_on_disk"] = True + with open(test_skill.settings.path, 'w') as f: + json.dump(skill_settings, f, indent=2) + + self.assertTrue(settings_changed.wait(5)) + on_file_change.assert_called_once_with(test_skill.settings.path) def test_upload_settings(self): # TODO pass def test_handle_settings_file_change(self): - # TODO - pass + real_upload = self.skill._upload_settings + self.skill._upload_settings = Mock() + settings_file = self.skill.settings.path + + # Handle change with no callback + self.skill._handle_settings_file_change(settings_file) + self.skill._upload_settings.assert_called_once() + + # Handle change with callback + self.skill._upload_settings.reset_mock() + self.skill.settings_change_callback = Mock() + self.skill._handle_settings_file_change(settings_file) + self.skill._upload_settings.assert_called_once() + self.skill.settings_change_callback.assert_called_once() + + # Handle non-settings file change + self.skill._handle_settings_file_change(join(dirname(settings_file), + "test.file")) + self.skill._upload_settings.assert_called_once() + self.skill.settings_change_callback.assert_called_once() + + self.skill._upload_settings = real_upload def test_load_lang(self): # TODO From bf1e5a3eedf579f67a51b3384c05fecbff7ec87f Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 18:09:53 +0000 Subject: [PATCH 127/154] Increment Version to 0.0.12a42 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index c8fe91ad..d76b0fa6 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 41 +VERSION_ALPHA = 42 # END_VERSION_BLOCK From c1549e15a1acd91eb3447195985ee6a3a456a9ef Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 18:10:29 +0000 Subject: [PATCH 128/154] Update Changelog --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f60db3..4d8192cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [0.0.12a42](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a42) (2023-07-13) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a41...0.0.12a42) + +**Closed issues:** + +- Since version 0.0.12a26, settings are not reloaded with my skill [\#91](https://github.com/OpenVoiceOS/OVOS-workshop/issues/91) + +**Merged pull requests:** + +- Unit tests and improvements to settings change callback [\#116](https://github.com/OpenVoiceOS/OVOS-workshop/pull/116) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a41](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a41) (2023-07-12) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a40...V0.0.12a41) + ## [V0.0.12a40](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a40) (2023-07-12) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a39...V0.0.12a40) @@ -616,14 +632,12 @@ - add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) - Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) -- add a busy page for common play [\#8](https://github.com/OpenVoiceOS/OVOS-workshop/pull/8) ([AIIX](https://github.com/AIIX)) **Fixed bugs:** - Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) - remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) - fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) -- fix icon paths and lower version [\#7](https://github.com/OpenVoiceOS/OVOS-workshop/pull/7) ([AIIX](https://github.com/AIIX)) **Merged pull requests:** From bb69a96014384815de2903d81da34a3ab5dd3cf5 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 13 Jul 2023 13:08:57 -0700 Subject: [PATCH 129/154] Fix language handling in intent and entity file resolution (#117) --- ovos_workshop/resource_files.py | 2 +- ovos_workshop/skills/base.py | 10 ++-- test/unittests/skills/test_base.py | 57 +++++++++++++++++-- .../test_locale/locale/en-us/dow.entity | 3 + .../test_locale/locale/en-us/time.intent | 1 + .../test_locale/locale/uk-ua/dow.entity | 3 + .../test_locale/locale/uk-ua/time.intent | 1 + .../test_mycroft_skill/test_mycroft_skill.py | 2 + 8 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 test/unittests/skills/test_locale/locale/en-us/dow.entity create mode 100644 test/unittests/skills/test_locale/locale/en-us/time.intent create mode 100644 test/unittests/skills/test_locale/locale/uk-ua/dow.entity create mode 100644 test/unittests/skills/test_locale/locale/uk-ua/time.intent diff --git a/ovos_workshop/resource_files.py b/ovos_workshop/resource_files.py index 21aa5df5..b146d12c 100644 --- a/ovos_workshop/resource_files.py +++ b/ovos_workshop/resource_files.py @@ -297,7 +297,7 @@ class ResourceFile: file_path: absolute path to the file """ - def __init__(self, resource_type, resource_name): + def __init__(self, resource_type: ResourceType, resource_name: str): self.resource_type = resource_type self.resource_name = resource_name self.file_path = self._locate() diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 1aef96bc..770b04f0 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -1599,8 +1599,8 @@ def register_intent_file(self, intent_file: str, handler: callable): """ for lang in self._native_langs: name = f'{self.skill_id}:{intent_file}' - resource_file = ResourceFile(self._resources.types.intent, - intent_file) + resources = self._load_lang(self.res_dir, lang) + resource_file = ResourceFile(resources.types.intent, intent_file) if resource_file.file_path is None: self.log.error(f'Unable to find "{intent_file}"') continue @@ -1628,12 +1628,14 @@ def register_entity_file(self, entity_file: str): if entity_file.endswith('.entity'): entity_file = entity_file.replace('.entity', '') for lang in self._native_langs: - entity = ResourceFile(self._resources.types.entity, entity_file) + resources = self._load_lang(self.res_dir, lang) + entity = ResourceFile(resources.types.entity, entity_file) if entity.file_path is None: self.log.error(f'Unable to find "{entity_file}"') continue filename = str(entity.file_path) - name = f"{self.skill_id}:{basename(entity_file)}_{md5(entity_file.encode('utf-8')).hexdigest()}" + name = f"{self.skill_id}:{basename(entity_file)}_" \ + f"{md5(entity_file.encode('utf-8')).hexdigest()}" self.intent_service.register_padatious_entity(name, filename, lang) def handle_enable_intent(self, message: Message): diff --git a/test/unittests/skills/test_base.py b/test/unittests/skills/test_base.py index c56701de..6d139dab 100644 --- a/test/unittests/skills/test_base.py +++ b/test/unittests/skills/test_base.py @@ -335,12 +335,61 @@ def test_register_intent(self): pass def test_register_intent_file(self): - # TODO - pass + from ovos_workshop.skills.base import BaseSkill + skill = BaseSkill(bus=self.bus, skill_id=self.skill_id) + skill._lang_resources = dict() + skill.intent_service = Mock() + skill.res_dir = join(dirname(__file__), "test_locale") + en_intent_file = join(skill.res_dir, "locale", "en-us", "time.intent") + uk_intent_file = join(skill.res_dir, "locale", "uk-ua", "time.intent") + + # No secondary languages + skill.config_core["lang"] = "en-us" + skill.config_core["secondary_langs"] = [] + skill.register_intent_file("time.intent", Mock(__name__="test")) + skill.intent_service.register_padatious_intent.assert_called_once_with( + f"{skill.skill_id}:time.intent", en_intent_file, "en-us") + + # With secondary language + skill.intent_service.register_padatious_intent.reset_mock() + skill.config_core["secondary_langs"] = ["en-us", "uk-ua"] + skill.register_intent_file("time.intent", Mock(__name__="test")) + self.assertEqual( + skill.intent_service.register_padatious_intent.call_count, 2) + skill.intent_service.register_padatious_intent.assert_any_call( + f"{skill.skill_id}:time.intent", en_intent_file, "en-us") + skill.intent_service.register_padatious_intent.assert_any_call( + f"{skill.skill_id}:time.intent", uk_intent_file, "uk-ua") def test_register_entity_file(self): - # TODO - pass + from ovos_workshop.skills.base import BaseSkill + skill = BaseSkill(bus=self.bus, skill_id=self.skill_id) + skill._lang_resources = dict() + skill.intent_service = Mock() + skill.res_dir = join(dirname(__file__), "test_locale") + en_file = join(skill.res_dir, "locale", "en-us", "dow.entity") + uk_file = join(skill.res_dir, "locale", "uk-ua", "dow.entity") + + # No secondary languages + skill.config_core["lang"] = "en-us" + skill.config_core["secondary_langs"] = [] + skill.register_entity_file("dow") + skill.intent_service.register_padatious_entity.assert_called_once_with( + f"{skill.skill_id}:dow_d446b2a6e46e7d94cdf7787e21050ff9", + en_file, "en-us") + + # With secondary language + skill.intent_service.register_padatious_entity.reset_mock() + skill.config_core["secondary_langs"] = ["en-us", "uk-ua"] + skill.register_entity_file("dow") + self.assertEqual( + skill.intent_service.register_padatious_entity.call_count, 2) + skill.intent_service.register_padatious_entity.assert_any_call( + f"{skill.skill_id}:dow_d446b2a6e46e7d94cdf7787e21050ff9", + en_file, "en-us") + skill.intent_service.register_padatious_entity.assert_any_call( + f"{skill.skill_id}:dow_d446b2a6e46e7d94cdf7787e21050ff9", + uk_file, "uk-ua") def test_handle_enable_intent(self): # TODO diff --git a/test/unittests/skills/test_locale/locale/en-us/dow.entity b/test/unittests/skills/test_locale/locale/en-us/dow.entity new file mode 100644 index 00000000..7cbf6bfc --- /dev/null +++ b/test/unittests/skills/test_locale/locale/en-us/dow.entity @@ -0,0 +1,3 @@ +monday +tuesday +wednesday \ No newline at end of file diff --git a/test/unittests/skills/test_locale/locale/en-us/time.intent b/test/unittests/skills/test_locale/locale/en-us/time.intent new file mode 100644 index 00000000..5ebaa610 --- /dev/null +++ b/test/unittests/skills/test_locale/locale/en-us/time.intent @@ -0,0 +1 @@ +what time is it \ No newline at end of file diff --git a/test/unittests/skills/test_locale/locale/uk-ua/dow.entity b/test/unittests/skills/test_locale/locale/uk-ua/dow.entity new file mode 100644 index 00000000..7cbf6bfc --- /dev/null +++ b/test/unittests/skills/test_locale/locale/uk-ua/dow.entity @@ -0,0 +1,3 @@ +monday +tuesday +wednesday \ No newline at end of file diff --git a/test/unittests/skills/test_locale/locale/uk-ua/time.intent b/test/unittests/skills/test_locale/locale/uk-ua/time.intent new file mode 100644 index 00000000..ac9796fe --- /dev/null +++ b/test/unittests/skills/test_locale/locale/uk-ua/time.intent @@ -0,0 +1 @@ +котра година \ No newline at end of file diff --git a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py index c5f1a639..f0962fed 100644 --- a/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py +++ b/test/unittests/skills/test_mycroft_skill/test_mycroft_skill.py @@ -550,6 +550,8 @@ def test_two_anonymous_intent_decorators(self): class _TestSkill(MycroftSkill): def __init__(self): super().__init__() + self.config_core['lang'] = "en-us" + self.config_core['secondary_langs'] = [] self.skill_id = 'A' From 5e17e7d8711fcfe42572afad5a037e6bc3805497 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 20:09:15 +0000 Subject: [PATCH 130/154] Increment Version to 0.0.12a43 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index d76b0fa6..a25b6c54 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 42 +VERSION_ALPHA = 43 # END_VERSION_BLOCK From f43f81cbf328a02cc4677eb7345d5e2a66e30b84 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 20:09:55 +0000 Subject: [PATCH 131/154] Update Changelog --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d8192cb..2cba6cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a42](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a42) (2023-07-13) +## [0.0.12a43](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a43) (2023-07-13) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a41...0.0.12a42) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a42...0.0.12a43) + +**Merged pull requests:** + +- Fix language handling in intent and entity file resolution [\#117](https://github.com/OpenVoiceOS/OVOS-workshop/pull/117) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a42](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a42) (2023-07-13) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a41...V0.0.12a42) **Closed issues:** @@ -631,7 +639,6 @@ **Implemented enhancements:** - add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) -- Add media service based video player and seek controls [\#9](https://github.com/OpenVoiceOS/OVOS-workshop/pull/9) ([AIIX](https://github.com/AIIX)) **Fixed bugs:** From f6a6950a1fa400206b20a29484f8dfcdd5050372 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Thu, 13 Jul 2023 14:22:57 -0700 Subject: [PATCH 132/154] Update OVOSAbstractApplication and unit tests (#118) --- ovos_workshop/app.py | 8 +++++--- test/unittests/test_abstract_app.py | 22 +++++++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/ovos_workshop/app.py b/ovos_workshop/app.py index d49e9fc1..b4f134c5 100644 --- a/ovos_workshop/app.py +++ b/ovos_workshop/app.py @@ -14,7 +14,7 @@ def __init__(self, skill_id: str, bus: Optional[MessageBusClient] = None, resources_dir: Optional[str] = None, lang=None, settings: Optional[dict] = None, gui: Optional[GUIInterface] = None, - enable_settings_manager: bool = False): + enable_settings_manager: bool = False, **kwargs): """ Create an Application. An application is essentially a skill, but designed such that it may be run without an intent service. @@ -28,8 +28,10 @@ def __init__(self, skill_id: str, bus: Optional[MessageBusClient] = None, @param enable_settings_manager: if True, enables a SettingsManager for this application to manage default settings and backend sync """ - super().__init__(bus=bus, gui=gui, resources_dir=resources_dir, - enable_settings_manager=enable_settings_manager) + super().__init__(skill_id=skill_id, bus=bus, gui=gui, + resources_dir=resources_dir, + enable_settings_manager=enable_settings_manager, + **kwargs) self.skill_id = skill_id self._dedicated_bus = False if bus: diff --git a/test/unittests/test_abstract_app.py b/test/unittests/test_abstract_app.py index 38847a43..1cf65145 100644 --- a/test/unittests/test_abstract_app.py +++ b/test/unittests/test_abstract_app.py @@ -26,11 +26,8 @@ class TestApp(unittest.TestCase): gui = GUIInterface("TestApplication") - @classmethod - def setUpClass(cls) -> None: - cls.app = Application(skill_id="TestApplication", - settings=cls.settings_obj, gui=cls.gui) - cls.app._startup(cls.bus) + app = Application(skill_id="TestApplication", settings=settings_obj, + gui=gui, bus=bus) def test_settings_manager_init(self): self.assertIsNone(self.app.settings_manager) @@ -44,9 +41,8 @@ def test_settings_init(self): self.assertFalse(self.app.settings['updated']) def test_settings_init_invalid_arg(self): - app = Application(skill_id="TestApplication", + app = Application(skill_id="TestApplication", bus=self.bus, settings=self.settings) - app._startup(self.bus) self.assertNotEqual(app.settings, self.settings) self.assertFalse(app.settings['__mycroft_skill_firstrun']) @@ -57,14 +53,10 @@ def test_settings_path(self): self.assertIn("/apps/", self.app._settings_path) # Test settings path conflicts - test_app = OVOSAbstractApplication(skill_id="test") + test_app = OVOSAbstractApplication(skill_id="test", bus=self.bus) from ovos_workshop.skills import OVOSSkill, MycroftSkill - test_skill = OVOSSkill() - mycroft_skill = MycroftSkill() - - test_app._startup(self.bus, "test") - test_skill._startup(self.bus, "test") - mycroft_skill._startup(self.bus, "test") + test_skill = OVOSSkill(skill_id="test", bus=self.bus) + mycroft_skill = MycroftSkill(skill_id="test", bus=self.bus) # Test app vs skill base directories self.assertIn("/apps/", test_app._settings_path) @@ -93,7 +85,7 @@ def test_default_shutdown(self, skill_shutdown): self.app.clear_intents = Mock() self.app.default_shutdown() self.app.clear_intents.assert_called_once() - self.app.bus.close.assert_called_once() + self.app.bus.close.assert_not_called() # No dedicated bus here skill_shutdown.assert_called_once() self.app.bus.close = real_bus_close From 21a40d0b25a4499c46574056fd950293b4ed2a0e Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 21:23:15 +0000 Subject: [PATCH 133/154] Increment Version to 0.0.12a44 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index a25b6c54..d769a45b 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 43 +VERSION_ALPHA = 44 # END_VERSION_BLOCK From 9611b61ac00a0763e68c930cc274ec776df7ccc6 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Thu, 13 Jul 2023 21:23:53 +0000 Subject: [PATCH 134/154] Update Changelog --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cba6cb0..93e79ac0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a43](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a43) (2023-07-13) +## [0.0.12a44](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a44) (2023-07-13) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a42...0.0.12a43) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a43...0.0.12a44) + +**Merged pull requests:** + +- Update OVOSAbstractApplication and unit tests [\#118](https://github.com/OpenVoiceOS/OVOS-workshop/pull/118) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a43](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a43) (2023-07-13) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a42...V0.0.12a43) **Merged pull requests:** @@ -644,7 +652,6 @@ - Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) - remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) -- fix end of media state [\#10](https://github.com/OpenVoiceOS/OVOS-workshop/pull/10) ([AIIX](https://github.com/AIIX)) **Merged pull requests:** From 0d1fce9851cd87bf3fdbb57f4ed8538d61ce3198 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Fri, 14 Jul 2023 15:02:20 +0100 Subject: [PATCH 135/154] fix init (#120) --- ovos_workshop/skills/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ovos_workshop/skills/base.py b/ovos_workshop/skills/base.py index 770b04f0..83184236 100644 --- a/ovos_workshop/skills/base.py +++ b/ovos_workshop/skills/base.py @@ -212,8 +212,8 @@ def __init__(self, name: Optional[str] = None, self.__original_converse = self.converse # yay, following python best practices again! - if self.skill_id and self.bus: - self._startup(self.bus, self.skill_id) + if self.skill_id and bus: + self._startup(bus, self.skill_id) @classproperty def runtime_requirements(self) -> RuntimeRequirements: From b20331cb8cb6b500e4b26ef76dfe0c2e9346d58d Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 14 Jul 2023 14:02:36 +0000 Subject: [PATCH 136/154] Increment Version to 0.0.12a45 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index d769a45b..aff9c402 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 44 +VERSION_ALPHA = 45 # END_VERSION_BLOCK From fb3f947d88f427712589868d187d4e0ddfdf06a7 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Fri, 14 Jul 2023 14:03:12 +0000 Subject: [PATCH 137/154] Update Changelog --- CHANGELOG.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e79ac0..0484ab86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # Changelog -## [0.0.12a44](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a44) (2023-07-13) +## [0.0.12a45](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a45) (2023-07-14) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a43...0.0.12a44) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a44...0.0.12a45) + +**Fixed bugs:** + +- PHAL and admin PHAL failures to successfully initialize started evening of Jul 13 [\#119](https://github.com/OpenVoiceOS/OVOS-workshop/issues/119) +- fix init [\#120](https://github.com/OpenVoiceOS/OVOS-workshop/pull/120) ([JarbasAl](https://github.com/JarbasAl)) + +## [V0.0.12a44](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a44) (2023-07-13) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a43...V0.0.12a44) **Merged pull requests:** @@ -651,12 +660,10 @@ **Fixed bugs:** - Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) -- remove forced focus event to allow page swipes [\#11](https://github.com/OpenVoiceOS/OVOS-workshop/pull/11) ([AIIX](https://github.com/AIIX)) **Merged pull requests:** - Feat/workflows [\#16](https://github.com/OpenVoiceOS/OVOS-workshop/pull/16) ([JarbasAl](https://github.com/JarbasAl)) -- feat/pypi\_automation [\#13](https://github.com/OpenVoiceOS/OVOS-workshop/pull/13) ([JarbasAl](https://github.com/JarbasAl)) From 215867dc857099030104bb6f417a1ebdfadc1b6d Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Fri, 14 Jul 2023 17:39:47 -0700 Subject: [PATCH 138/154] Loosen ovos-backend-client dependency to allow latest stable version (#121) --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 42f45afc..89d55318 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,5 +2,5 @@ ovos-utils < 0.1.0, >=0.0.35a7 ovos_config < 0.1.0,>=0.0.5 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.5 -ovos_backend_client>=0.1.0a6 +ovos_backend_client~=0.0,>=0.0.6 rapidfuzz From 6fabbd741c035702bd93e982495dfaad70fe1b3a Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 15 Jul 2023 00:40:03 +0000 Subject: [PATCH 139/154] Increment Version to 0.0.12a46 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index aff9c402..86d14877 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 45 +VERSION_ALPHA = 46 # END_VERSION_BLOCK From a0848164522397077f5f1f38d632bb9106183448 Mon Sep 17 00:00:00 2001 From: JarbasAl Date: Sat, 15 Jul 2023 00:40:38 +0000 Subject: [PATCH 140/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0484ab86..ac4891ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a45](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a45) (2023-07-14) +## [0.0.12a46](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a46) (2023-07-15) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a44...0.0.12a45) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a45...0.0.12a46) + +**Merged pull requests:** + +- Loosen ovos-backend-client dependency to allow latest stable version [\#121](https://github.com/OpenVoiceOS/OVOS-workshop/pull/121) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a45](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a45) (2023-07-14) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a44...V0.0.12a45) **Fixed bugs:** @@ -653,10 +661,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/d9261b124f73a3e4d50c6edfcd9c2243b2bc3cf6...V0.0.5a12) -**Implemented enhancements:** - -- add idleDisplaySkill type [\#14](https://github.com/OpenVoiceOS/OVOS-workshop/pull/14) ([AIIX](https://github.com/AIIX)) - **Fixed bugs:** - Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) From 2f014c00da2ba6423b1fc21e30ee2a0558116159 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Tue, 18 Jul 2023 19:52:11 -0700 Subject: [PATCH 141/154] Minor import and logging updates to troubleshoot logged warnings (#122) * Update internal import to avoid deprecation warning Add `skill_id` to logged warnings to diagnose legacy behavior * Fix init kwarg handling in `OVOSSkill` subclasses * Refactor init for `initialize` backwards-compat. * Refactor CommonQuerySkill init to resolve deprecation warnings * Formatting cleanup * Fix fallback init bug Cleanup unused added import --- ovos_workshop/skills/common_play.py | 8 +++--- ovos_workshop/skills/common_query_skill.py | 31 ++++++++++++---------- ovos_workshop/skills/fallback.py | 8 +++--- ovos_workshop/skills/mycroft_skill.py | 9 ++++--- ovos_workshop/skills/ovos.py | 2 +- 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/ovos_workshop/skills/common_play.py b/ovos_workshop/skills/common_play.py index 50a4a413..884569c2 100644 --- a/ovos_workshop/skills/common_play.py +++ b/ovos_workshop/skills/common_play.py @@ -50,8 +50,7 @@ def ... vocab for starting playback is needed. """ - def __init__(self, name=None, bus=None): - super().__init__(name, bus) + def __init__(self, name=None, bus=None, **kwargs): # NOTE: derived skills will likely want to override this list self.supported_media = [MediaType.GENERIC, MediaType.AUDIO] @@ -66,7 +65,10 @@ def __init__(self, name=None, bus=None): self._stop_event = Event() self._playing = Event() # TODO replace with new default - self.skill_icon = "https://github.com/OpenVoiceOS/ovos-ocp-audio-plugin/raw/master/ovos_plugin_common_play/ocp/res/ui/images/ocp.png" + self.skill_icon = \ + "https://github.com/OpenVoiceOS/ovos-ocp-audio-plugin/raw/master/" \ + "ovos_plugin_common_play/ocp/res/ui/images/ocp.png" + OVOSSkill.__init__(self, name, bus, **kwargs) def bind(self, bus): """Overrides the normal bind method. diff --git a/ovos_workshop/skills/common_query_skill.py b/ovos_workshop/skills/common_query_skill.py index 1bb4fa2f..9fdd052b 100644 --- a/ovos_workshop/skills/common_query_skill.py +++ b/ovos_workshop/skills/common_query_skill.py @@ -16,7 +16,6 @@ from ovos_utils.file_utils import resolve_resource_file from ovos_utils.log import LOG - from ovos_workshop.skills.ovos import OVOSSkill, is_classic_core @@ -56,25 +55,29 @@ class CommonQuerySkill(OVOSSkill): answers from several skills presenting the best one available. """ - def __init__(self, name=None, bus=None): - super().__init__(name, bus) - noise_words_filepath = f"text/{self.lang}/noise_words.list" - default_res = f"{dirname(dirname(__file__))}/res/text/{self.lang}/noise_words.list" - noise_words_filename = resolve_resource_file(noise_words_filepath) or \ - resolve_resource_file(default_res) - - self._translated_noise_words = {} - if noise_words_filename: - with open(noise_words_filename) as f: - translated_noise_words = f.read().strip() - self._translated_noise_words[self.lang] = translated_noise_words.split() - + def __init__(self, name=None, bus=None, **kwargs): # these should probably be configurable self.level_confidence = { CQSMatchLevel.EXACT: 0.9, CQSMatchLevel.CATEGORY: 0.6, CQSMatchLevel.GENERAL: 0.5 } + OVOSSkill.__init__(self, name, bus, **kwargs) + + noise_words_filepath = f"text/{self.lang}/noise_words.list" + default_res = f"{dirname(dirname(__file__))}/res/text/{self.lang}" \ + f"/noise_words.list" + noise_words_filename = \ + resolve_resource_file(noise_words_filepath, + config=self.config_core) or \ + resolve_resource_file(default_res, config=self.config_core) + + self._translated_noise_words = {} + if noise_words_filename: + with open(noise_words_filename) as f: + translated_noise_words = f.read().strip() + self._translated_noise_words[self.lang] = \ + translated_noise_words.split() @property def translated_noise_words(self): diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 87af5619..5bb80148 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -99,11 +99,11 @@ class FallbackSkillV1(_MetaFB, metaclass=_MutableFallback): fallback_handlers = {} wrapper_map: List[Tuple[callable, callable]] = [] # [(handler, wrapper)] - def __init__(self, name=None, bus=None, use_settings=True): - super().__init__(name, bus, use_settings) + def __init__(self, name=None, bus=None, use_settings=True, **kwargs): # list of fallback handlers registered by this instance self.instance_fallback_handlers = [] + super().__init__(name, bus, use_settings, **kwargs) # "skill_id": priority (int) overrides self.fallback_config = self.config_core["skills"].get("fallbacks", {}) @@ -312,9 +312,9 @@ def make_intent_failure_handler(cls, bus: MessageBusClient): """ return FallbackSkillV1.make_intent_failure_handler(bus) - def __init__(self, bus=None, skill_id=""): + def __init__(self, bus=None, skill_id="", **kwargs): self._fallback_handlers = [] - super().__init__(bus=bus, skill_id=skill_id) + super().__init__(bus=bus, skill_id=skill_id, **kwargs) @property def priority(self) -> int: diff --git a/ovos_workshop/skills/mycroft_skill.py b/ovos_workshop/skills/mycroft_skill.py index 4ea70744..c7cfcd2f 100644 --- a/ovos_workshop/skills/mycroft_skill.py +++ b/ovos_workshop/skills/mycroft_skill.py @@ -96,10 +96,11 @@ def __call__(cls, *args, **kwargs): # accepts kwargs and does its own init return super().__call__(skill_id=skill_id, bus=bus, **kwargs) except TypeError: - LOG.warning("legacy skill signature detected, attempting to init " - "skill manually, self.bus and self.skill_id will only " - "be available in self.initialize.\n__init__ method " - "needs to accept `skill_id` and `bus` to resolve this.") + LOG.warning(f"Legacy skill signature detected for {skill_id};" + f" attempting to init skill manually, self.bus and " + f"self.skill_id will only be available in " + f"self.initialize. `__init__` method needs to accept " + f"`skill_id` and `bus` to resolve this.") # skill did not update its init method, init it manually # NOTE: no try/except because all skills must accept this initialization diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 65707178..8fa7c5cb 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -13,7 +13,7 @@ from ovos_utils.sound import play_audio from ovos_workshop.resource_files import SkillResources -from ovos_workshop.skills.layers import IntentLayers +from ovos_workshop.decorators.layers import IntentLayers from ovos_workshop.skills.mycroft_skill import MycroftSkill, is_classic_core From 834480271f15e47bddebe86bd32e6eab94fd2f11 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Wed, 19 Jul 2023 02:52:29 +0000 Subject: [PATCH 142/154] Increment Version to 0.0.12a47 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 86d14877..1601028f 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 46 +VERSION_ALPHA = 47 # END_VERSION_BLOCK From 532ba57699ba031ff2dbe20012209052d5fdd407 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Wed, 19 Jul 2023 02:53:03 +0000 Subject: [PATCH 143/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac4891ca..b76a4632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a46](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a46) (2023-07-15) +## [0.0.12a47](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a47) (2023-07-19) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a45...0.0.12a46) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a46...0.0.12a47) + +**Merged pull requests:** + +- Minor import and logging updates to troubleshoot logged warnings [\#122](https://github.com/OpenVoiceOS/OVOS-workshop/pull/122) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a46](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a46) (2023-07-15) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a45...V0.0.12a46) **Merged pull requests:** @@ -661,10 +669,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/d9261b124f73a3e4d50c6edfcd9c2243b2bc3cf6...V0.0.5a12) -**Fixed bugs:** - -- Fix/idleskill [\#15](https://github.com/OpenVoiceOS/OVOS-workshop/pull/15) ([NeonJarbas](https://github.com/NeonJarbas)) - **Merged pull requests:** - Feat/workflows [\#16](https://github.com/OpenVoiceOS/OVOS-workshop/pull/16) ([JarbasAl](https://github.com/JarbasAl)) From c54e4cd3b6d73cede50984b5d12c96d37e01d09b Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:33:35 -0700 Subject: [PATCH 144/154] Fix dependency installation in codecov automation (#123) Update unit tests to only upload coverage for one run --- .github/workflows/coverage.yml | 11 ++++------- .github/workflows/unit_tests.yml | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 5e97e0e7..c6fec4ee 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,18 +21,15 @@ jobs: sudo apt-get update sudo apt install python3-dev python -m pip install build wheel + - name: Install repo + run: | + pip install -e . - name: Install test dependencies run: | sudo apt install libssl-dev libfann-dev portaudio19-dev libpulse-dev - pip install ovos-core[all] - pip install pytest pytest-timeout pytest-cov adapt-parser~=0.5 - - name: Install repo - run: | - pip install . + pip install -r requirements/test.txt - name: Generate coverage report run: | - pip install pytest - pip install pytest-cov pytest --cov=./ovos_workshop --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 22c6eac0..fbdd1d15 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -61,6 +61,7 @@ jobs: # or they will overwrite previous invocations' coverage reports # (for an example, see OVOS Skill Manager's workflow) - name: Upload coverage + if: "${{ matrix.python-version == '3.9' }}" env: CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} uses: codecov/codecov-action@v2 From 26b21189b73cef1f8d8ba201e069f51ff085c134 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:25:45 -0700 Subject: [PATCH 145/154] Update dependencies to stable versions (#124) --- requirements/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 89d55318..ed721d82 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ -ovos-utils < 0.1.0, >=0.0.35a7 -ovos_config < 0.1.0,>=0.0.5 +ovos-utils < 0.1.0, >=0.0.35 +ovos_config < 0.1.0,>=0.0.10 ovos-lingua-franca~=0.4,>=0.4.6 ovos-bus-client < 0.1.0, >=0.0.5 ovos_backend_client~=0.0,>=0.0.6 From e6c30ee487c9244eee3f7b3752f053c14b5784e6 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:26:07 +0000 Subject: [PATCH 146/154] Increment Version to 0.0.12a48 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 1601028f..e5a0a045 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 47 +VERSION_ALPHA = 48 # END_VERSION_BLOCK From b2e01f1f852cc2ade7b8ac13e7583ad26fc9534f Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:26:43 +0000 Subject: [PATCH 147/154] Update Changelog --- CHANGELOG.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b76a4632..c304a1cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,17 @@ # Changelog -## [0.0.12a47](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a47) (2023-07-19) +## [0.0.12a48](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a48) (2023-07-20) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a46...0.0.12a47) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a47...0.0.12a48) + +**Merged pull requests:** + +- Update dependencies to stable versions [\#124](https://github.com/OpenVoiceOS/OVOS-workshop/pull/124) ([NeonDaniel](https://github.com/NeonDaniel)) +- Fix codecov automation [\#123](https://github.com/OpenVoiceOS/OVOS-workshop/pull/123) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a47](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a47) (2023-07-19) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a46...V0.0.12a47) **Merged pull requests:** @@ -657,10 +666,6 @@ - remove/skillgui\_patches [\#18](https://github.com/OpenVoiceOS/OVOS-workshop/pull/18) ([JarbasAl](https://github.com/JarbasAl)) -**Closed issues:** - -- OVOSSkill class inherited skills do not initialize [\#17](https://github.com/OpenVoiceOS/OVOS-workshop/issues/17) - ## [V0.0.5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5) (2022-02-25) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5a12...V0.0.5) @@ -669,10 +674,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/d9261b124f73a3e4d50c6edfcd9c2243b2bc3cf6...V0.0.5a12) -**Merged pull requests:** - -- Feat/workflows [\#16](https://github.com/OpenVoiceOS/OVOS-workshop/pull/16) ([JarbasAl](https://github.com/JarbasAl)) - \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 5c11215ed6a3ff178d098dd5c401b0c123d422fc Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:41:18 -0700 Subject: [PATCH 148/154] Update automation to latest standards (#125) --- .github/workflows/build_tests.yml | 56 --------------------------- .github/workflows/dev2master.yml | 20 ---------- .github/workflows/license_tests.yml | 40 ++----------------- .github/workflows/publish_alpha.yml | 43 ++++---------------- .github/workflows/publish_release.yml | 40 ++----------------- .github/workflows/unit_tests.yml | 5 +++ setup.py | 1 + 7 files changed, 20 insertions(+), 185 deletions(-) delete mode 100644 .github/workflows/build_tests.yml delete mode 100644 .github/workflows/dev2master.yml diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml deleted file mode 100644 index 6764d4b7..00000000 --- a/.github/workflows/build_tests.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Run Build Tests -on: - push: - branches: - - master - pull_request: - branches: - - dev - paths-ignore: - - 'ovos_workshop/version.py' - - 'test/**' - - 'examples/**' - - '.github/**' - - '.gitignore' - - 'LICENSE' - - 'CHANGELOG.md' - - 'MANIFEST.in' - - 'readme.md' - - 'scripts/**' - workflow_dispatch: - -jobs: - build_tests: - strategy: - matrix: - python-version: [ 3.7, 3.8, 3.9, "3.10", "3.11" ] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev swig libssl-dev - - name: Build Source Packages - run: | - python setup.py sdist - - name: Build Distribution Packages - run: | - python setup.py bdist_wheel - - name: Install package - run: | - pip install .[all] - - uses: pypa/gh-action-pip-audit@v1.0.7 - with: - # Ignore setuptools vulnerability we can't do much about - ignore-vulns: | - GHSA-r9hx-vwmv-q579 - PYSEC-2023-74 - PYSEC-2022-43012 \ No newline at end of file diff --git a/.github/workflows/dev2master.yml b/.github/workflows/dev2master.yml deleted file mode 100644 index cc76fee2..00000000 --- a/.github/workflows/dev2master.yml +++ /dev/null @@ -1,20 +0,0 @@ -# This workflow will generate a distribution and upload it to PyPI - -name: Push dev -> master -on: - workflow_dispatch: - -jobs: - build_and_publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - ref: dev - - name: Push dev -> master - uses: ad-m/github-push-action@master - - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - branch: master \ No newline at end of file diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml index 29f4063e..7d0c4f6b 100644 --- a/.github/workflows/license_tests.yml +++ b/.github/workflows/license_tests.yml @@ -1,44 +1,10 @@ name: Run License Tests on: push: - branches: - - master + workflow_dispatch: pull_request: branches: - - dev - workflow_dispatch: - + - master jobs: license_tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v1 - with: - python-version: 3.8 - - name: Install Build Tools - run: | - python -m pip install build wheel - - name: Install System Dependencies - run: | - sudo apt-get update - sudo apt install python3-dev swig libssl-dev - - name: Install core repo - run: | - pip install . - - name: Get explicit and transitive dependencies - run: | - pip freeze > requirements-all.txt - - name: Check python - id: license_check_report - uses: pilosus/action-pip-license-checker@v0.5.0 - with: - requirements: 'requirements-all.txt' - fail: 'Copyleft,Other,Error' - fails-only: true - exclude: '^(tqdm).*' - exclude-license: '^(Mozilla).*$' - - name: Print report - if: ${{ always() }} - run: echo "${{ steps.license_check_report.outputs.report }}" \ No newline at end of file + uses: neongeckocom/.github/.github/workflows/license_tests.yml@master diff --git a/.github/workflows/publish_alpha.yml b/.github/workflows/publish_alpha.yml index ad6f76cc..efed3b01 100644 --- a/.github/workflows/publish_alpha.yml +++ b/.github/workflows/publish_alpha.yml @@ -19,44 +19,15 @@ on: workflow_dispatch: jobs: - update_version: - uses: neongeckocom/.github/.github/workflows/propose_semver_release.yml@master + publish_alpha_release: + uses: neongeckocom/.github/.github/workflows/publish_alpha_release.yml@master + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} with: - release_type: "alpha" - version_file: ovos_workshop/version.py + version_file: "ovos_workshop/version.py" + publish_prerelease: true + update_changelog: true alpha_var: VERSION_ALPHA build_var: VERSION_BUILD minor_var: VERSION_MINOR major_var: VERSION_MAJOR - update_changelog: True - branch: dev - build_and_publish: - runs-on: ubuntu-latest - needs: update_version - steps: - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ needs.update_version.outputs.version }} - release_name: Release ${{ needs.update_version.outputs.version }} - body: | - Changes in this Release - ${{ needs.update_version.outputs.changelog }} - draft: false - prerelease: true - commitish: dev - - name: Checkout Repository - uses: actions/checkout@v2 - with: - ref: dev - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: Build Distribution Packages - run: | - python setup.py sdist bdist_wheel - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{secrets.PYPI_TOKEN}} diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index edb91cdc..185756ad 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -5,39 +5,7 @@ on: - master jobs: - github_release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: master - fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. - - name: version - run: echo "::set-output name=version::$(python setup.py --version)" - id: version - - name: "Generate release changelog" - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - id: changelog - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: V${{ steps.version.outputs.version }} - release_name: Release ${{ steps.version.outputs.version }} - body: | - Changes in this Release - ${{ steps.changelog.outputs.changelog }} - draft: false - prerelease: false - commitish: master - - name: Build Distribution Packages - run: | - python setup.py sdist bdist_wheel - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{secrets.PYPI_TOKEN}} \ No newline at end of file + build_and_publish_pypi_and_release: + uses: neongeckocom/.github/.github/workflows/publish_stable_release.yml@master + secrets: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index fbdd1d15..9d5c634b 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -31,6 +31,11 @@ on: workflow_dispatch: jobs: + py_build_tests: + uses: neongeckocom/.github/.github/workflows/python_build_tests.yml@master + with: + python_version: "3.8" + unit_tests: strategy: matrix: diff --git a/setup.py b/setup.py index d5022914..6a90fa2c 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ from setuptools import setup BASEDIR = os.path.abspath(os.path.dirname(__file__)) +os.chdir(BASEDIR) # For relative `packages` spec in setup below def get_version(): From 2060e5993e7aff34401c98c39dfacf99d02cca6c Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:41:37 +0000 Subject: [PATCH 149/154] Increment Version to 0.0.12a49 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index e5a0a045..18fcffa0 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 48 +VERSION_ALPHA = 49 # END_VERSION_BLOCK From e6465bad4bd6d5d404b990d6f037b06664c208dc Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:42:07 +0000 Subject: [PATCH 150/154] Update Changelog --- CHANGELOG.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c304a1cc..de97b563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog -## [0.0.12a48](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a48) (2023-07-20) +## [0.0.12a49](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a49) (2023-07-20) -[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a47...0.0.12a48) +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a48...0.0.12a49) + +**Merged pull requests:** + +- Update automation to latest standards [\#125](https://github.com/OpenVoiceOS/OVOS-workshop/pull/125) ([NeonDaniel](https://github.com/NeonDaniel)) + +## [V0.0.12a48](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.12a48) (2023-07-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a47...V0.0.12a48) **Merged pull requests:** @@ -662,10 +670,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5...V0.0.6a1) -**Breaking changes:** - -- remove/skillgui\_patches [\#18](https://github.com/OpenVoiceOS/OVOS-workshop/pull/18) ([JarbasAl](https://github.com/JarbasAl)) - ## [V0.0.5](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.5) (2022-02-25) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.5a12...V0.0.5) From 7606662e91af5b15a8bd4c5af4d8d3709c98b993 Mon Sep 17 00:00:00 2001 From: Daniel McKnight <34697904+NeonDaniel@users.noreply.github.com> Date: Wed, 19 Jul 2023 18:51:36 -0700 Subject: [PATCH 151/154] Add description to setup.py to fix #125 (#126) * Add description to setup.py to fix #125 * Add trivial README.md --- README.md | 3 +++ setup.py | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..960d9c63 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# OVOS Workshop +OVOS Workshop contains skill base classes and supporting tools to build skills +and applications for OpenVoiceOS systems. diff --git a/setup.py b/setup.py index 6a90fa2c..03b7e232 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,12 @@ def required(requirements_file): if pkg.strip() and not pkg.startswith("#")] +def get_description(): + with open(os.path.join(BASEDIR, "README.md"), "r") as f: + long_description = f.read() + return long_description + + setup( name='ovos_workshop', version=get_version(), @@ -67,6 +73,8 @@ def required(requirements_file): author_email='jarbasai@mailfence.com', include_package_data=True, description='frameworks, templates and patches for the OpenVoiceOS universe', + long_description=get_description(), + long_description_content_type="text/markdown", entry_points={ 'console_scripts': [ 'ovos-skill-launcher=ovos_workshop.skill_launcher:_launch_script' From 3f7a87678aee53b1d0330f7c5dcd84fc4adccc4f Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:51:54 +0000 Subject: [PATCH 152/154] Increment Version to 0.0.12a50 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index 18fcffa0..c61229c0 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 49 +VERSION_ALPHA = 50 # END_VERSION_BLOCK From cd002b7a0a1874d914655cc3655f49a13682c1cb Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 01:52:23 +0000 Subject: [PATCH 153/154] Update Changelog --- CHANGELOG.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de97b563..cc358eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.0.12a50](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a50) (2023-07-20) + +[Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/0.0.12a49...0.0.12a50) + +**Merged pull requests:** + +- Add description to setup.py to fix \#125 [\#126](https://github.com/OpenVoiceOS/OVOS-workshop/pull/126) ([NeonDaniel](https://github.com/NeonDaniel)) + ## [0.0.12a49](https://github.com/OpenVoiceOS/OVOS-workshop/tree/0.0.12a49) (2023-07-20) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.12a48...0.0.12a49) @@ -658,10 +666,6 @@ [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6...V0.0.7a1) -**Fixed bugs:** - -- Fix/optional adapt [\#19](https://github.com/OpenVoiceOS/OVOS-workshop/pull/19) ([JarbasAl](https://github.com/JarbasAl)) - ## [V0.0.6](https://github.com/OpenVoiceOS/OVOS-workshop/tree/V0.0.6) (2022-03-03) [Full Changelog](https://github.com/OpenVoiceOS/OVOS-workshop/compare/V0.0.6a1...V0.0.6) From e8540c0c68345d7f230a71e836b13a387ea2b477 Mon Sep 17 00:00:00 2001 From: NeonDaniel Date: Thu, 20 Jul 2023 02:27:47 +0000 Subject: [PATCH 154/154] Increment Version to 0.0.12 --- ovos_workshop/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovos_workshop/version.py b/ovos_workshop/version.py index c61229c0..04f75bc2 100644 --- a/ovos_workshop/version.py +++ b/ovos_workshop/version.py @@ -3,5 +3,5 @@ VERSION_MAJOR = 0 VERSION_MINOR = 0 VERSION_BUILD = 12 -VERSION_ALPHA = 50 +VERSION_ALPHA = 0 # END_VERSION_BLOCK