diff --git a/mycroft/audio/__main__.py b/mycroft/audio/__main__.py index 340137207745..be7379480641 100644 --- a/mycroft/audio/__main__.py +++ b/mycroft/audio/__main__.py @@ -24,7 +24,7 @@ ) from mycroft.util.log import LOG from mycroft.util.process_utils import ProcessStatus, StatusCallbackMap - +from mycroft.configuration import setup_locale import mycroft.audio.speech as speech from mycroft.audio.audioservice import AudioService @@ -53,6 +53,7 @@ def main(ready_hook=on_ready, error_hook=on_error, stopping_hook=on_stopping): on_stopping=stopping_hook) status = ProcessStatus('audio', bus, callbacks) + setup_locale() speech.init(bus) # Connect audio service instance to message bus diff --git a/mycroft/client/enclosure/__main__.py b/mycroft/client/enclosure/__main__.py index f7bdb5dde9b6..1e3ef9a64117 100644 --- a/mycroft/client/enclosure/__main__.py +++ b/mycroft/client/enclosure/__main__.py @@ -17,7 +17,7 @@ This provides any "enclosure" specific functionality, for example GUI or control over the Mark-1 Faceplate. """ -from mycroft.configuration import LocalConf, SYSTEM_CONFIG +from mycroft.configuration import LocalConf, SYSTEM_CONFIG, setup_locale from mycroft.util.log import LOG from mycroft.util import wait_for_exit_signal, reset_sigint_handler @@ -78,6 +78,7 @@ def main(ready_hook=on_ready, error_hook=on_error, stopping_hook=on_stopping): LOG.debug("Enclosure created") try: reset_sigint_handler() + setup_locale() enclosure.run() ready_hook() wait_for_exit_signal() diff --git a/mycroft/client/speech/__main__.py b/mycroft/client/speech/__main__.py index 8f362a1a0a73..6d6e5abb710b 100644 --- a/mycroft/client/speech/__main__.py +++ b/mycroft/client/speech/__main__.py @@ -17,7 +17,7 @@ from mycroft import dialog from mycroft.enclosure.api import EnclosureAPI from mycroft.client.speech.listener import RecognizerLoop -from mycroft.configuration import Configuration +from mycroft.configuration import Configuration, setup_locale from mycroft.identity import IdentityManager from mycroft.lock import Lock as PIDLock # Create/Support PID locking file from mycroft.messagebus.message import Message @@ -228,7 +228,7 @@ def main(ready_hook=on_ready, error_hook=on_error, stopping_hook=on_stopping, callbacks = StatusCallbackMap(on_ready=ready_hook, on_error=error_hook, on_stopping=stopping_hook) status = ProcessStatus('speech', bus, callbacks) - + setup_locale() # Register handlers on internal RecognizerLoop bus loop = RecognizerLoop(watchdog) connect_loop_events(loop) diff --git a/mycroft/client/text/__main__.py b/mycroft/client/text/__main__.py index 3e04f537b08a..4da64c8d87e3 100644 --- a/mycroft/client/text/__main__.py +++ b/mycroft/client/text/__main__.py @@ -23,7 +23,7 @@ start_log_monitor, start_mic_monitor, connect_to_mycroft, ctrl_c_handler ) -from mycroft.configuration import Configuration +from mycroft.configuration import Configuration, setup_locale sys.stdout = io.StringIO() sys.stderr = io.StringIO() @@ -54,6 +54,7 @@ def main(): start_mic_monitor(os.path.join(get_ipc_directory(), "mic_level")) connect_to_mycroft() + setup_locale() if '--simple' in sys.argv: sys.stdout = sys.__stdout__ sys.stderr = sys.__stderr__ diff --git a/mycroft/configuration/__init__.py b/mycroft/configuration/__init__.py index 9f741660eadb..4a22989fb4f1 100644 --- a/mycroft/configuration/__init__.py +++ b/mycroft/configuration/__init__.py @@ -13,5 +13,7 @@ # limitations under the License. # from mycroft.configuration.config import Configuration, LocalConf, RemoteConf -from mycroft.configuration.locale import set_default_lf_lang +from mycroft.configuration.locale import set_default_lf_lang, setup_locale, \ + set_default_tz, set_default_lang, get_default_tz, get_default_lang, \ + get_config_tz, get_primary_lang_code, load_languages, load_language from mycroft.configuration.locations import SYSTEM_CONFIG, USER_CONFIG diff --git a/mycroft/configuration/locale.py b/mycroft/configuration/locale.py index 8ffc613e8f9e..1d916211fb06 100644 --- a/mycroft/configuration/locale.py +++ b/mycroft/configuration/locale.py @@ -18,10 +18,127 @@ The mycroft.util.lang module provides the main interface for setting up the lingua-franca (https://github.com/mycroftai/lingua-franca) selected language """ +from dateutil.tz import gettz, tzlocal -from lingua_franca import set_default_lang as _set_default_lf_lang +# we want to support both LF and LN +# when using the mycroft wrappers this is not an issue, but skills might use +# either, so mycroft-lib needs to account for this and set the defaults for +# both libs. +# lingua_franca/lingua_nostra are optional and might not be installed +# exceptions should only be raised in the parse and format utils + +try: + import lingua_franca as LF +except ImportError: + LF = None + +try: + import lingua_nostra as LN +except ImportError: + LN = None + +_lang = "en-us" +_tz = tzlocal() + + +def get_primary_lang_code(): + if LN: + return LN.get_primary_lang_code() + if LF: + return LF.get_primary_lang_code() + return _lang.split("-")[0] + + +def get_default_lang(): + if LN: + return LN.get_default_lang() + if LF: + return LF.get_default_lang() + return _lang + + +def set_default_lang(lang): + global _lang + _lang = lang + if LN: + LN.set_default_lang(lang) + if LF: + LF.set_default_lang(lang) + + +def get_config_tz(): + # TODO cyclic import error because top module is called "locale" + from mycroft.configuration.config import Configuration + config = Configuration.get() + code = config["location"]["timezone"]["code"] + return gettz(code) + + +def get_default_tz(): + tz = None + # go with LF/LN default timezone, in case skills changed it temporarily + # for some arcane reason... + if LN: + tz = tz or LN.time.default_timezone() + if LF: + try: + tz = tz or LF.time.default_timezone() + except Exception: + # old versions of LF + # AttributeError: module 'lingua_franca' has no attribute 'time' + pass + + # use the timezone from .conf + tz = tz or get_config_tz() + + # Just go with global default + return tz or _tz + + +def set_default_tz(tz=None): + """ configure both LF and LN """ + global _tz + tz = tz or get_config_tz() or tzlocal() + _tz = tz + if LN: + LN.time.set_default_tz(tz) + if LF: + # tz added in recently, depends on version + try: + LF.time.set_default_tz(tz) + except: + pass + + +def load_languages(langs): + if LN: + LN.load_languages(langs) + if LF: + LF.load_languages(langs) + + +def load_language(lang): + if LN: + LN.load_language(lang) + if LF: + LF.load_language(lang) + + +def setup_locale(lang=None, tz=None): + # TODO cyclic import error because top module is called "locale" + from mycroft.configuration.config import Configuration + lang_code = lang or Configuration.get().get("lang", "en-us") + # Load language resources, currently en-us must also be loaded at all times + load_languages([lang_code, "en-us"]) + # Set the active lang to match the configured one + set_default_lang(lang_code) + # Set the default timezone to match the configured one + set_default_tz(tz) + + +# mycroft-core backwards compat LF only interface def set_default_lf_lang(lang_code="en-us"): """Set the default language of Lingua Franca for parsing and formatting. @@ -32,4 +149,4 @@ def set_default_lf_lang(lang_code="en-us"): Args: lang (str): BCP-47 language code, e.g. "en-us" or "es-mx" """ - return _set_default_lf_lang(lang_code=lang_code) + return set_default_lang(lang_code) \ No newline at end of file diff --git a/mycroft/skills/__main__.py b/mycroft/skills/__main__.py index d2ba6250ac10..c62dc154ce29 100644 --- a/mycroft/skills/__main__.py +++ b/mycroft/skills/__main__.py @@ -22,14 +22,13 @@ from threading import Event from msm.exceptions import MsmException -from lingua_franca import load_languages import mycroft.lock from mycroft import dialog from mycroft.api import is_paired, BackendDown, DeviceApi from mycroft.audio import wait_while_speaking from mycroft.enclosure.api import EnclosureAPI -from mycroft.configuration import Configuration +from mycroft.configuration import Configuration, setup_locale from mycroft.messagebus.message import Message from mycroft.util import ( connected, @@ -199,8 +198,7 @@ def main(alive_hook=on_alive, started_hook=on_started, ready_hook=on_ready, # Create PID file, prevent multiple instances of this service mycroft.lock.Lock('skills') config = Configuration.get() - lang_code = config.get("lang", "en-us") - load_languages([lang_code, "en-us"]) + setup_locale() # Connect this process to the Mycroft message bus bus = start_message_bus_client("SKILLS") diff --git a/mycroft/skills/intent_service.py b/mycroft/skills/intent_service.py index ab2d5ac0a9a3..08e4a5975b1d 100644 --- a/mycroft/skills/intent_service.py +++ b/mycroft/skills/intent_service.py @@ -16,7 +16,7 @@ from copy import copy import time -from mycroft.configuration import Configuration, set_default_lf_lang +from mycroft.configuration import Configuration, setup_locale from mycroft.util.log import LOG from mycroft.util.parse import normalize from mycroft.metrics import report_timing, Stopwatch @@ -33,7 +33,7 @@ def _get_message_lang(message): message: message to check for language code. Returns: - The languge code from the message or the default language. + The language code from the message or the default language. """ default_lang = Configuration.get().get('lang', 'en-us') return message.data.get('lang', default_lang).lower() @@ -152,7 +152,7 @@ def get_skill_name(self, skill_id): def reset_converse(self, message): """Let skills know there was a problem with speech recognition""" lang = _get_message_lang(message) - set_default_lf_lang(lang) + setup_locale(lang) # restore default lang for skill in copy(self.active_skills): self.do_converse(None, skill[0], lang, message) @@ -273,7 +273,7 @@ def handle_utterance(self, message): """ try: lang = _get_message_lang(message) - set_default_lf_lang(lang) + setup_locale(lang) # set default lang utterances = message.data.get('utterances', []) combined = _normalize_all_utterances(utterances) diff --git a/mycroft/util/format.py b/mycroft/util/format.py index b37bf8fa453c..a8422a07efaf 100644 --- a/mycroft/util/format.py +++ b/mycroft/util/format.py @@ -30,23 +30,44 @@ from calendar import leapdays from enum import Enum -# These are the main functions we are using lingua franca to provide -# TODO 21.08 - move nice_duration methods to Lingua Franca. -from lingua_franca.format import ( - join_list, - nice_date, - nice_date_time, - nice_number, - nice_time, - nice_year, - pronounce_number -) -# TODO 21.08 - remove import of private method _translate_word -# Consider whether the remaining items here are necessary. -from lingua_franca.format import (NUMBER_TUPLE, DateTimeFormat, - date_time_format, expand_options, - _translate_word) -from padatious.util import expand_parentheses +from mycroft.util.bracket_expansion import expand_parentheses, expand_options +from mycroft.configuration.locale import get_default_lang + + +# lingua_franca is optional, both lingua_franca and lingua_nostra are supported +# if both are installed preference is given to LN +# "setters" will be set in both lbs +# LN should be functionality equivalent to LF + +try: + try: + from lingua_nostra.format import (NUMBER_TUPLE, DateTimeFormat, + join_list, + date_time_format, expand_options, + _translate_word, + nice_number, nice_time, + pronounce_number, + nice_date, nice_date_time, nice_year) + except ImportError: + # These are the main functions we are using lingua franca to provide + from lingua_franca.format import (NUMBER_TUPLE, DateTimeFormat, + join_list, + date_time_format, expand_options, + _translate_word, + nice_number, nice_time, + pronounce_number, + nice_date, nice_date_time, nice_year) +except ImportError: + def lingua_franca_error(*args, **kwargs): + raise ImportError("lingua_franca is not installed") + + from mycroft.util.bracket_expansion import expand_options + + NUMBER_TUPLE, DateTimeFormat = None, None + + join_list = date_time_format = _translate_word = nice_number = \ + nice_time = pronounce_number = nice_date = nice_date_time = \ + nice_year = lingua_franca_error class TimeResolution(Enum): diff --git a/mycroft/util/parse.py b/mycroft/util/parse.py index 233004a4ac88..82f5b32b6ef2 100644 --- a/mycroft/util/parse.py +++ b/mycroft/util/parse.py @@ -26,24 +26,34 @@ do most of the actual parsing. However methods may be wrapped specifically for use in Mycroft Skills. """ - -from difflib import SequenceMatcher from warnings import warn -from lingua_franca.parse import ( - extract_duration, - extract_number, - extract_numbers, - fuzzy_match, - get_gender, - match_one, - normalize, -) -from lingua_franca.parse import extract_datetime as _extract_datetime +# lingua_franca is optional, both lingua_franca and lingua_nostra are supported +# if both are installed preference is given to LN +# "setters" will be set in both lbs +# LN should be functionality equivalent to LF from mycroft.util.time import now_local from mycroft.util.log import LOG +try: + try: + from lingua_nostra.parse import extract_number, extract_numbers, \ + extract_duration, get_gender, normalize + from lingua_nostra.parse import extract_datetime as lf_extract_datetime + from lingua_nostra.time import now_local + except ImportError: + from lingua_franca.parse import extract_number, extract_numbers, \ + extract_duration, get_gender, normalize + from lingua_franca.parse import extract_datetime as lf_extract_datetime + from lingua_franca.time import now_local +except ImportError: + def lingua_franca_error(*args, **kwargs): + raise ImportError("lingua_franca is not installed") + + extract_number = extract_numbers = extract_duration = get_gender = \ + normalize = lf_extract_datetime = lingua_franca_error + def _log_unsupported_language(language, supported_languages): """ @@ -115,4 +125,4 @@ def extract_datetime(text, anchorDate="DEFAULT", lang=None, "deprecated. This parameter can be omitted.")) if anchorDate is None or anchorDate == "DEFAULT": anchorDate = now_local() - return _extract_datetime(text, anchorDate, lang, default_time) + return lf_extract_datetime(text, anchorDate, lang, default_time) diff --git a/mycroft/util/time.py b/mycroft/util/time.py index 0c86e0ad8db6..1faa5ffc85ca 100644 --- a/mycroft/util/time.py +++ b/mycroft/util/time.py @@ -20,7 +20,24 @@ from datetime import datetime from dateutil.tz import gettz, tzlocal +# LN/LF are optional and should not be needed because of time utils +# only parse and format utils require LN/LF +# NOTE: lingua_franca has some bad UTC assumptions in date conversions, +# for that reason we do not import from there in this case + +try: + import lingua_franca as LF +except ImportError: + LF = None + +try: + import lingua_nostra as LN +except ImportError: + LN = None + + +# backwards compat import, recommend using get_default_tz instead def default_timezone(): """Get the default timezone @@ -30,19 +47,8 @@ def default_timezone(): Returns: (datetime.tzinfo): Definition of the default timezone """ - try: - # Obtain from user's configurated settings - # location.timezone.code (e.g. "America/Chicago") - # location.timezone.name (e.g. "Central Standard Time") - # location.timezone.offset (e.g. -21600000) - from mycroft.configuration import Configuration - config = Configuration.get() - code = config["location"]["timezone"]["code"] - - return gettz(code) - except Exception: - # Just go with system default timezone - return tzlocal() + from mycroft.configuration.locale import get_default_tz + return get_default_tz() def now_utc(): @@ -63,8 +69,7 @@ def now_local(tz=None): Returns: (datetime): The current time """ - if not tz: - tz = default_timezone() + tz = tz or default_timezone() return datetime.now(tz) @@ -76,26 +81,24 @@ def to_utc(dt): Returns: (datetime): time converted to UTC """ - tzUTC = gettz("UTC") - if dt.tzinfo: - return dt.astimezone(tzUTC) - else: - return dt.replace(tzinfo=gettz("UTC")).astimezone(tzUTC) + tz = gettz("UTC") + if not dt.tzinfo: + dt = dt.replace(tzinfo=default_timezone()) + return dt.astimezone(tz) def to_local(dt): """Convert a datetime to the user's local timezone - Args: - dt (datetime): A datetime (if no timezone, defaults to UTC) - Returns: - (datetime): time converted to the local timezone - """ + Args: + dt (datetime): A datetime (if no timezone, defaults to UTC) + Returns: + (datetime): time converted to the local timezone + """ tz = default_timezone() - if dt.tzinfo: - return dt.astimezone(tz) - else: - return dt.replace(tzinfo=gettz("UTC")).astimezone(tz) + if not dt.tzinfo: + dt = dt.replace(tzinfo=default_timezone()) + return dt.astimezone(tz) def to_system(dt): @@ -107,7 +110,6 @@ def to_system(dt): (datetime): time converted to the operation system's timezone """ tz = tzlocal() - if dt.tzinfo: - return dt.astimezone(tz) - else: - return dt.replace(tzinfo=gettz("UTC")).astimezone(tz) + if not dt.tzinfo: + dt = dt.replace(tzinfo=default_timezone()) + return dt.astimezone(tz)