diff --git a/documentation/_build/doctrees/config.doctree b/documentation/_build/doctrees/config.doctree new file mode 100644 index 00000000..d26a5c76 Binary files /dev/null and b/documentation/_build/doctrees/config.doctree differ diff --git a/documentation/_build/doctrees/configure.doctree b/documentation/_build/doctrees/configure.doctree new file mode 100644 index 00000000..8cad1c33 Binary files /dev/null and b/documentation/_build/doctrees/configure.doctree differ diff --git a/documentation/_build/doctrees/developers.doctree b/documentation/_build/doctrees/developers.doctree new file mode 100644 index 00000000..b351dd60 Binary files /dev/null and b/documentation/_build/doctrees/developers.doctree differ diff --git a/documentation/_build/doctrees/documentation.doctree b/documentation/_build/doctrees/documentation.doctree new file mode 100644 index 00000000..51c79150 Binary files /dev/null and b/documentation/_build/doctrees/documentation.doctree differ diff --git a/documentation/_build/doctrees/environment.pickle b/documentation/_build/doctrees/environment.pickle new file mode 100644 index 00000000..75b14005 Binary files /dev/null and b/documentation/_build/doctrees/environment.pickle differ diff --git a/documentation/_build/doctrees/index.doctree b/documentation/_build/doctrees/index.doctree new file mode 100644 index 00000000..c5eb2928 Binary files /dev/null and b/documentation/_build/doctrees/index.doctree differ diff --git a/documentation/_build/doctrees/install.doctree b/documentation/_build/doctrees/install.doctree new file mode 100644 index 00000000..df008350 Binary files /dev/null and b/documentation/_build/doctrees/install.doctree differ diff --git a/documentation/_build/doctrees/loader.doctree b/documentation/_build/doctrees/loader.doctree new file mode 100644 index 00000000..b40e9e60 Binary files /dev/null and b/documentation/_build/doctrees/loader.doctree differ diff --git a/documentation/_build/doctrees/modules.doctree b/documentation/_build/doctrees/modules.doctree new file mode 100644 index 00000000..5022edab Binary files /dev/null and b/documentation/_build/doctrees/modules.doctree differ diff --git a/documentation/_build/doctrees/natlinkstatus.doctree b/documentation/_build/doctrees/natlinkstatus.doctree new file mode 100644 index 00000000..e6d240ef Binary files /dev/null and b/documentation/_build/doctrees/natlinkstatus.doctree differ diff --git a/documentation/_build/doctrees/project.doctree b/documentation/_build/doctrees/project.doctree new file mode 100644 index 00000000..3db1f6ec Binary files /dev/null and b/documentation/_build/doctrees/project.doctree differ diff --git a/documentation/_build/html/.buildinfo b/documentation/_build/html/.buildinfo new file mode 100644 index 00000000..d3e6e80a --- /dev/null +++ b/documentation/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 6d468b6d747b898509175a32b8c01478 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/documentation/_build/html/_modules/index.html b/documentation/_build/html/_modules/index.html new file mode 100644 index 00000000..a36b3168 --- /dev/null +++ b/documentation/_build/html/_modules/index.html @@ -0,0 +1,105 @@ + + +
+ + +
+#pylint:disable=C0114, C0115, C0116, R0913, E1101
+import configparser
+import logging
+import os
+from enum import IntEnum
+from typing import List, Iterable, Dict
+import natlink
+
+
+
+[docs]class LogLevel(IntEnum):
+ CRITICAL = logging.CRITICAL
+ FATAL = logging.FATAL
+ ERROR = logging.ERROR
+ WARNING = logging.WARNING
+ INFO = logging.INFO
+ DEBUG = logging.DEBUG
+ NOTSET = logging.NOTSET
+
+
+class NatlinkConfig:
+ def __init__(self, directories_by_user: Dict[str, List[str]], log_level: LogLevel, load_on_mic_on: bool,
+ load_on_begin_utterance: bool, load_on_startup: bool, load_on_user_changed: bool):
+ self.directories_by_user = directories_by_user # maps user profile names to directories, '' for global
+ self.log_level = log_level
+ self.load_on_mic_on = load_on_mic_on
+ self.load_on_begin_utterance = load_on_begin_utterance
+ self.load_on_startup = load_on_startup
+ self.load_on_user_changed = load_on_user_changed
+ self.config_path = '' # to be defined in from_config_parser
+
+ def __repr__(self) -> str:
+ return f'NatlinkConfig(directories_by_user={self.directories_by_user}, log_level={self.log_level}, ' \
+ f'load_on_mic_on={self.load_on_mic_on}, load_on_startup={self.load_on_startup}, ' \
+ f'load_on_user_changed={self.load_on_user_changed}'
+
+ @staticmethod
+ def get_default_config() -> 'NatlinkConfig':
+ return NatlinkConfig(directories_by_user=dict(),
+ log_level=LogLevel.NOTSET,
+ load_on_mic_on=True,
+ load_on_begin_utterance=False,
+ load_on_startup=True,
+ load_on_user_changed=True)
+
+ @property
+ def directories(self) -> List[str]:
+ dirs: List[str] = []
+ for _u, directories in self.directories_by_user.items():
+ dirs.extend(directories)
+ return dirs
+
+ def directories_for_user(self, user: str) -> List[str]:
+ dirs: List[str] = []
+ for u, directories in self.directories_by_user.items():
+ if u in ['', user]:
+ dirs.extend(directories)
+ return dirs
+
+ @staticmethod
+ def from_config_parser(config: configparser.ConfigParser, config_path: str) -> 'NatlinkConfig':
+ ret = NatlinkConfig.get_default_config()
+ ret.config_path = config_path
+ sections = config.sections()
+ for section in sections:
+ if section.endswith('-directories'):
+ user = section[:-len('-directories')]
+ ret.directories_by_user[user] = list(config[section].values())
+ elif section == 'directories':
+ directories = []
+ for name, directory in config[section].items():
+ ## allow environment variables (or ~) in directory
+ directory_expanded = expand_path(directory)
+ if not os.path.isdir(directory_expanded):
+ if directory_expanded == directory:
+ print(f'from_config_parser: skip "{directory}" ("{name}"): is not a valid directory')
+ else:
+ print(f'from_config_parser: skip "{directory}" ("{name}"):\n\texpanded to directory "{directory_expanded}" is not a valid directory')
+ continue
+ directories.append(directory_expanded)
+
+ ret.directories_by_user[''] = directories
+ if config.has_section('settings'):
+ settings = config['settings']
+ level = settings.get('log_level')
+ if level is not None:
+ ret.log_level = LogLevel[level.upper()]
+ ret.load_on_mic_on = settings.getboolean('load_on_mic_on', fallback=ret.load_on_mic_on)
+ ret.load_on_begin_utterance = settings.getboolean('load_on_begin_utterance',
+ fallback=ret.load_on_begin_utterance)
+ ret.load_on_startup = settings.getboolean('load_on_startup', fallback=ret.load_on_startup)
+ ret.load_on_user_changed = settings.getboolean('load_on_user_changed', fallback=ret.load_on_user_changed)
+
+ return ret
+
+ @classmethod
+ def from_file(cls, fn: str) -> 'NatlinkConfig':
+ return cls.from_first_found_file([fn])
+
+ @classmethod
+ def from_first_found_file(cls, files: Iterable[str]) -> 'NatlinkConfig':
+ isfile = os.path.isfile
+ config = configparser.ConfigParser()
+ for fn in files:
+ if not isfile(fn):
+ continue
+ if config.read(fn):
+ return cls.from_config_parser(config, config_path=fn)
+ # should not happen, because of DefaultConfig (was InstallTest)
+ raise NoGoodConfigFoundException('No natlink config file found, please run configure natlink program\n\t(configurenatlink.pyw or natlinkconfigfunctions.py)')
+
+[docs]def expand_path(input_path: str) -> str:
+ r"""expand path if it starts with "~" or has environment variables (%XXXX%)
+
+ Use home ("~") or "%natlink_userdir%"
+
+ The Documents directory can be found by "~\Documents"
+
+ When nothing to expand, return input
+ """
+ expanduser, expandvars, normpath = os.path.expanduser, os.path.expandvars, os.path.normpath
+
+ if input_path.startswith('~'):
+ home = expanduser('~')
+ env_expanded = home + input_path[1:]
+ # print(f'expand_path: "{input_path}" include "~": expanded: "{env_expanded}"')
+ return normpath(env_expanded)
+ env_expanded = expandvars(input_path)
+ # print(f'env_expanded: "{env_expanded}", from envvar: "{input_path}"')
+ return normpath(env_expanded)
+
+
+
+
+#pylint:disable=C0114, C0115, C0116, R1705, R0902, R0904, R0912, R0915, W0703, E1101
+import importlib
+import importlib.machinery
+import importlib.util
+import logging
+import os
+import sys
+import sysconfig
+import time
+import traceback
+import winreg
+import configparser
+from pathlib import Path
+from types import ModuleType
+from typing import List, Dict, Set, Iterable, Any, Tuple, Callable
+
+import natlink
+from natlink.config import LogLevel, NatlinkConfig, expand_path
+from natlink.readwritefile import ReadWriteFile
+from natlink.callbackhandler import CallbackHandler
+from natlink.singleton import Singleton
+# the possible languages (for get_user_language) (runs at start and on_change_callback, user)
+# default is "enx", being one of the English dialects...
+UserLanguages = {
+ "Nederlands": "nld",
+ "Fran\xe7ais": "fra",
+ "Deutsch": "deu",
+ "Italiano": "ita",
+ "Espa\xf1ol": "esp",
+ "Dutch": "nld",
+ "French": "fra",
+ "German": "deu",
+ "Italian": "ita",
+ "Spanish": "esp",}
+
+[docs]class NatlinkMain(metaclass=Singleton):
+ """main class of Natlink, make it a "singleton"
+ """
+
+ def __init__(self, logger: Any=None, config: Any = None):
+ if logger is None:
+ raise ValueError(f'loader.NatlinkMain, first instance should be called with a logging.Logger instance, not {logger}')
+ if config is None:
+ raise ValueError(f'loader.NatlinkMain, first instance should be called with a NatlinkConfig instance, not {config}')
+ self.logger = logger
+ self.config = config
+ self.loaded_modules: Dict[Path, ModuleType] = {}
+ self.prog_names_visited: Set[str] = set() # to enable loading program specific grammars
+ self.bad_modules: Set[Path] = set()
+ self.load_attempt_times: Dict[Path, float] = {}
+ self.__user: str = '' #
+ self.__profile: str = '' # at start and on_change_callback user
+ self.__language: str = '' #
+ self.__load_on_begin_utterance = None
+ self.load_on_begin_utterance = self.config.load_on_begin_utterance # set the property load_on_begin_utterance
+ # callback instances:
+ self._pre_load_callback = CallbackHandler('pre_load')
+ self._post_load_callback = CallbackHandler('post_load')
+ self._on_mic_on_callback = CallbackHandler('on_mic_on')
+ self._on_begin_utterance_callback = CallbackHandler('on_begin_utterance')
+ self.seen: Set[Path] = set() # start empty in trigger_load
+ self.bom = self.encoding = self.config_text = '' # getconfigsetting and writeconfigsetting
+
+ def set_on_begin_utterance_callback(self, func: Callable[[], None]) -> None:
+ self._on_begin_utterance_callback.set(func)
+
+ def set_on_mic_on_callback(self, func: Callable[[], None]) -> None:
+ self._on_mic_on_callback.set(func)
+
+ def set_pre_load_callback(self, func: Callable[[], None]) -> None:
+ self._pre_load_callback.set(func)
+
+ def set_post_load_callback(self, func: Callable[[], None]) -> None:
+ self._post_load_callback.set(func)
+
+ def delete_on_begin_utterance_callback(self, func: Callable[[], None]) -> None:
+ self._on_begin_utterance_callback.delete(func)
+
+ def delete_on_mic_on_callback(self, func: Callable[[], None]) -> None:
+ self._on_mic_on_callback.delete(func)
+
+ def delete_pre_load_callback(self, func: Callable[[], None]) -> None:
+ self._pre_load_callback.delete(func)
+
+ def delete_post_load_callback(self, func: Callable[[], None]) -> None:
+ self._post_load_callback.delete(func)
+
+ @property
+ def module_paths_for_user(self) -> List[Path]:
+ return self._module_paths_in_dirs(self.config.directories_for_user(self.user))
+
+ # @property
+ # def module_paths_for_directory(self) -> List[Path]:
+ # return self._module_paths_in_dir(self.config.directories_for_user(self.user))
+
+ # three properties, which are set at start or at on_change_callback:
+ @property
+ def language(self) -> str:
+ """holds the language of the current profile (default 'enx')
+ """
+ if self.__language == '':
+ self.set_user_language()
+
+ return self.__language or 'enx'
+
+ @language.setter
+ def language(self, value: str):
+ if value and len(value) == 3:
+ self.__language = value
+ else:
+ self.__language = 'enx'
+ self.logger.warning(f'set language property: invalid value ("{value}"), set "enx"')
+
+ @property
+ def profile(self) -> str:
+ """holds the directory profile of current user profile
+ """
+ return self.__profile or ''
+
+ @profile.setter
+ def profile(self, value: str):
+ self.__profile = value or ''
+
+ @property
+ def user(self) -> str:
+ """holds the name of the current user profile
+ """
+ return self.__user or ''
+
+ @user.setter
+ def user(self, value: str):
+ self.__user = value or ''
+
+ # load_on_begin_utterance is a property...
+[docs] def get_load_on_begin_utterance(self) -> Any:
+ """this value is most often True or False, taken from the config file
+
+ It can also be (set to) a positive int, with which it does
+ the load_on_begin_utterance so many times. After these utterances,
+ the value falls back to False.
+
+ With Vocola, there is one utterance delay in the updating of the changed vocola command files.
+ """
+ return self.__load_on_begin_utterance
+
+[docs] def set_load_on_begin_utterance(self, value: Any):
+ """set the value for loading at each utterance to True, False or positive int
+
+ For Vocola, setting this value to 1 did not work, setting to 2 does, so
+ you need one extra utterance for a new vocola command to come through.
+ """
+ if isinstance(value, int):
+ if value > 0:
+ self.logger.info(f'set_load_on_begin_utterance to {value}')
+ self.__load_on_begin_utterance = value
+ else:
+ self.logger.info('set_load_on_begin_utterance to False')
+ self.__load_on_begin_utterance = False
+ elif value in [True, False]:
+ self.__load_on_begin_utterance = value
+ else:
+ raise TypeError(f'set_load_on_begin_utterance, invalid type for value: {value} (type: {type(value)})')
+
+
+ load_on_begin_utterance = property(get_load_on_begin_utterance, set_load_on_begin_utterance)
+
+ # def _module_paths_in_dir(self, directory: str) -> List[Path]:
+ # """give modules in directory
+ # """
+ #
+ # def is_script(f: Path) -> bool:
+ # if not f.is_file():
+ # return False
+ # if not f.suffix == '.py':
+ # return False
+ #
+ # if f.stem.startswith('_'):
+ # return True
+ # for prog_name in self.prog_names_visited:
+ # if f.stem == prog_name or f.stem.startswith( prog_name + '_'):
+ # return True
+ # return False
+ #
+ # init = '__init__.py'
+ #
+ # mod_paths: List[Path] = []
+ # dir_path = Path(directory)
+ # scripts = sorted(filter(is_script, dir_path.iterdir()))
+ # init_path = dir_path.joinpath(init)
+ # if init_path in scripts:
+ # scripts.remove(init_path)
+ # scripts.insert(0, init_path)
+ # mod_paths.extend(scripts)
+ #
+ # return mod_paths
+
+ def _module_paths_in_dirs(self, directories: Iterable[str]) -> List[Path]:
+
+ def is_script(f: Path) -> bool:
+ if not f.is_file():
+ return False
+ if not f.suffix == '.py':
+ return False
+
+ if f.stem.startswith('_'):
+ return True
+ for prog_name in self.prog_names_visited:
+ if f.stem == prog_name or f.stem.startswith( prog_name + '_'):
+ return True
+ return False
+
+ init = '__init__.py'
+
+ mod_paths: List[Path] = []
+ for d in directories:
+ dir_path = Path(d)
+ scripts = sorted(filter(is_script, dir_path.iterdir()))
+ init_path = dir_path.joinpath(init)
+ if init_path in scripts:
+ scripts.remove(init_path)
+ scripts.insert(0, init_path)
+ mod_paths.extend(scripts)
+
+ return mod_paths
+
+ @staticmethod
+ def _add_dirs_to_path(directories: Iterable[str]) -> None:
+ for d in directories:
+ d_expanded = expand_path(d)
+ if d_expanded not in sys.path:
+ sys.path.insert(0, d_expanded)
+
+ def _call_and_catch_all_exceptions(self, fn: Callable[[], None]) -> None:
+ try:
+ fn()
+ except Exception:
+ self.logger.exception(traceback.format_exc())
+
+ def unload_module(self, module: ModuleType) -> None:
+ unload = getattr(module, 'unload', None)
+ if unload is not None:
+ self.logger.debug(f'unloading module: {module.__name__}')
+ self._call_and_catch_all_exceptions(unload)
+
+ @staticmethod
+ def _import_module_from_path(mod_path: Path) -> ModuleType:
+ mod_name = mod_path.stem
+ spec = importlib.util.spec_from_file_location(mod_name, mod_path)
+ if spec is None:
+ raise FileNotFoundError(f'Could not find spec for: {mod_name}')
+ loader = spec.loader
+ if loader is None:
+ raise FileNotFoundError(f'Could not find loader for: {mod_name}')
+ if not isinstance(loader, importlib.machinery.SourceFileLoader):
+ raise ValueError(f'module {mod_name} does not have a SourceFileLoader loader')
+ module = importlib.util.module_from_spec(spec)
+ loader.exec_module(module)
+ return module
+
+ def load_or_reload_module(self, mod_path: Path, force_load: bool = False) -> None:
+ mod_name = mod_path.stem
+ if mod_path in self.seen:
+ self.logger.warning(f'Attempting to load duplicate module: {mod_path})')
+ return
+
+ # if not self.load_attempt_times:
+ # self.logger.warning(f'======== load_attempt_times is empty: {self.load_attempt_times}')
+
+ last_attempt_time = self.load_attempt_times.get(mod_path, 0.0)
+ self.load_attempt_times[mod_path] = time.time()
+
+ try:
+ if mod_path in self.bad_modules:
+ self.logger.debug(f'mod_path: {mod_path}, in self.bad_modules...')
+ last_modified_time = mod_path.stat().st_mtime
+ if force_load or last_attempt_time < last_modified_time:
+ self.logger.info(f'loading previously bad module: {mod_name}')
+ module = self._import_module_from_path(mod_path)
+ try:
+ self.bad_modules.remove(mod_path)
+ except KeyError:
+ # added QH, I think it should not come here:
+ self.logger.warning(f'load_or_reload_module, unexpected, cannot remove key {mod_path} from self.bad_modules:\n\t{self.bad_modules}\n\t====\n')
+ self.loaded_modules[mod_path] = module
+ return
+ else:
+ self.logger.info(f'skipping unchanged bad module: {mod_name}')
+ return
+ else:
+ maybe_module = self.loaded_modules.get(mod_path)
+ if force_load or maybe_module is None:
+ self.logger.info(f'loading module: {mod_name}')
+ module = self._import_module_from_path(mod_path)
+ self.loaded_modules[mod_path] = module
+ return
+ else:
+ module = maybe_module
+ last_modified_time = mod_path.stat().st_mtime
+ diff = last_modified_time - last_attempt_time # check for -0.1 instead of 0, a ???
+ # _pre_load_callback may need this..
+ if force_load or diff > 0:
+ if force_load:
+ self.logger.info(f'reloading module: {mod_name}, force_load: {force_load}')
+ else:
+ self.logger.info(f'reloading module: {mod_name}')
+
+ self.unload_module(module)
+ del module
+ module = self._import_module_from_path(mod_path)
+ self.loaded_modules[mod_path] = module
+ self.logger.debug(f'loaded module: {module.__name__}')
+ return
+ else:
+ self.logger.debug(f'skipping unchanged loaded module: {mod_name}')
+ return
+ except Exception:
+ self.logger.exception(traceback.format_exc())
+ self.logger.debug(f'load_or_reload_module, exception, add to self.bad_modules {mod_path}')
+ self.bad_modules.add(mod_path)
+ if mod_path in self.loaded_modules:
+ old_module = self.loaded_modules.pop(mod_path)
+ self.unload_module(old_module)
+ del old_module
+ importlib.invalidate_caches()
+
+ def load_or_reload_modules(self, mod_paths: Iterable[Path], force_load: bool = None) -> None:
+ for mod_path in mod_paths:
+ self.load_or_reload_module(mod_path, force_load=force_load)
+ self.seen.add(mod_path)
+
+ def remove_modules_that_no_longer_exist(self) -> None:
+ mod_paths = self.module_paths_for_user
+
+ for mod_path in set(self.loaded_modules).difference(mod_paths):
+ self.logger.info(f'unloading removed or not-for-this-user module {mod_path.stem}')
+ old_module = self.loaded_modules.pop(mod_path)
+ self.load_attempt_times.pop(mod_path)
+ self.unload_module(old_module)
+ del old_module
+ for mod_path in self.bad_modules.difference(mod_paths):
+ self.logger.debug(f'bad module was removed: {mod_path.stem}')
+ self.bad_modules.remove(mod_path)
+ self.load_attempt_times.pop(mod_path)
+
+ importlib.invalidate_caches()
+
+ def trigger_load(self, force_load: bool = None) -> None:
+ self.seen.clear()
+ if force_load:
+ self.logger.debug(f'triggering load/reload process (force_load: {force_load})')
+ else:
+ self.logger.debug('triggering load/reload process')
+
+ self.remove_modules_that_no_longer_exist()
+
+ mod_paths = self.module_paths_for_user
+ self._pre_load_callback.run()
+ self.load_or_reload_modules(mod_paths, force_load=force_load)
+ self._post_load_callback.run()
+
+[docs] def on_change_callback(self, change_type: str, args: Any) -> None:
+ """on_change_callback, when another user profile is chosen, or when the mic state changes
+ """
+ if change_type == 'user':
+ self.set_user_language(args)
+ self.logger.debug(f'on_change_callback, user "{self.user}", profile: "{self.profile}", language: "{self.language}"')
+ if self.config.load_on_user_changed:
+ self.trigger_load(force_load=True)
+ elif change_type == 'mic' and args == 'on':
+ self.logger.debug('on_change_callback called with: "mic", "on"')
+ self._on_mic_on_callback.run()
+
+ if self.config.load_on_mic_on:
+ self.trigger_load()
+ else:
+ self.logger.debug(f'on_change_callback unhandled: change_type: "{change_type}", args: "{args}"')
+
+
+ def on_begin_callback(self, module_info: Tuple[str, str, int]) -> None:
+ self.logger.debug(f'on_begin_callback called with: moduleInfo: {module_info}')
+ self._on_begin_utterance_callback.run()
+
+ prog_name = Path(module_info[0]).stem
+ if prog_name not in self.prog_names_visited:
+ self.prog_names_visited.add(prog_name)
+ self.trigger_load()
+ elif self.load_on_begin_utterance:
+ # manipulate this setting:
+ value = self.load_on_begin_utterance
+ if isinstance(value, int):
+ value -= 1
+ value = value or False
+ self.load_on_begin_utterance = value
+ self.trigger_load()
+
+[docs] def get_user_language(self, DNSuserDirectory):
+ """return the user language (default "enx") from Dragon inifiles
+
+ like "nld" for Dutch, etc.
+ """
+ isfile, isdir, join = os.path.isfile, os.path.isdir, os.path.join
+
+ if not (DNSuserDirectory and isdir(DNSuserDirectory)):
+ self.logger.debug('get_user_language, no DNSuserDirectory passed, probably Dragon is not running, return "enx"')
+ return 'enx'
+
+ ns_options_ini = join(DNSuserDirectory, 'options.ini')
+ if not (ns_options_ini and isfile(ns_options_ini)):
+ self.logger.debug(f'get_user_language, warning no valid ini file: "{ns_options_ini}" found, return "enx"')
+ return "enx"
+
+ section = "Options"
+ keyname = "Last Used Acoustics"
+ keyToModel = self.getconfigsetting(option=keyname, section=section, filepath=ns_options_ini)
+
+ ns_acoustic_ini = join(DNSuserDirectory, 'acoustic.ini')
+ section = "Base Acoustic"
+ if not (ns_acoustic_ini and isfile(ns_acoustic_ini)):
+ self.logger.debug(f'get_user_language: warning: user language cannot be found from Dragon Inifile: "{ns_acoustic_ini}", return "enx"')
+ return 'enx'
+ # user_language_long = win32api.GetProfileVal(section, keyToModel, "", ns_acoustic_ini)
+ user_language_long = self.getconfigsetting(option=keyToModel, section=section, filepath=ns_acoustic_ini)
+ user_language_long = user_language_long.split("|")[0].strip()
+
+ if user_language_long in UserLanguages:
+ language = UserLanguages[user_language_long]
+ self.logger.debug(f'get_user_language, return "{language}", (long language: "{user_language_long}")')
+ else:
+ language = 'enx'
+ self.logger.debug(f'get_user_language, return userLanguage: "{language}", (long language: "{user_language_long}")')
+
+ return language
+
+[docs] def set_user_language(self, args: Any = None):
+ """can be called from other module to explicitly set the user language to 'enx', 'nld', etc
+ """
+ if not (args and len(args) == 2):
+ try:
+ args = natlink.getCurrentUser()
+ except natlink.NatError:
+ # when Dragon not running, for testing:
+ args = ()
+
+ if args:
+ self.user, self.profile = args
+ self.language = self.get_user_language(self.profile)
+ self.logger.debug(f'set_user_language, user: "{self.user}", profile: "{self.profile}", language: "{self.language}"')
+ else:
+ self.user, self.profile = '', ''
+ self.logger.warning('set_user_language, cannot get input for get_user_language, set to "enx",\n\tprobably Dragon is not running')
+ self.language = 'enx'
+
+
+ def start(self) -> None:
+ self.logger.info(f'starting natlink loader from config file:\n\t"{self.config.config_path}"')
+ natlink.active_loader = self
+ if not self.config.directories:
+ self.logger.warning(f'Starting Natlink, but no directories to load are specified.\n\tPlease add one or more directories\n\tin config file: "{self.config.config_path}".')
+ return
+ # self.logger.debug(f'directories: {self.config.directories}')
+ self._add_dirs_to_path(self.config.directories)
+ if self.config.load_on_startup:
+ # set language property:
+ self.set_user_language()
+ self.trigger_load()
+ natlink.setBeginCallback(self.on_begin_callback)
+ natlink.setChangeCallback(self.on_change_callback)
+
+ def setup_logger(self) -> None:
+ for handler in list(self.logger.handlers):
+ self.logger.removeHandler(handler)
+ self.logger.addHandler(logging.StreamHandler(sys.stdout))
+ self.logger.propagate = False
+ log_level = self.config.log_level
+ if log_level is not LogLevel.NOTSET:
+ self.logger.setLevel(log_level.value)
+ self.logger.debug(f'set log level to: {log_level.name}')
+
+[docs] def getconfigsetting(self, section: str, option: Any = None, filepath: Any = None, func: Any = None) -> str:
+ """get a setting from possibly an inifile other than natlink.ini
+
+ Take a string as input, which is obtained from readwritefile.py, handling
+ different encodings and possible BOM marks.
+
+ When no "option" is passed, the contents of the section are returned (a list of options)
+
+ func can be configparser.getint or configparser.getboolean if needed, otherwise configparser.get (str) is taken.
+ pass: func='getboolean' or func='getint'.
+ """
+ isfile = os.path.isfile
+ filepath = filepath or config_locations()[0]
+ if not isfile(filepath):
+ raise OSError(f'getconfigsetting, no valid filepath: "{filepath}"')
+ rwfile = ReadWriteFile()
+ self.config_text = rwfile.readAnything(filepath)
+ Config = configparser.ConfigParser()
+ Config.read_string(self.config_text)
+
+ if option is None:
+ return Config.options(section)
+
+ if isinstance(func, str):
+ func = getattr(Config, func)
+ else:
+ func = func or Config.get
+
+ if func.__name__ == 'get':
+ fallback = ''
+ elif func.__name__ == 'getint':
+ fallback = 0
+ elif func.__name__ == 'getboolean':
+ fallback = False
+ else:
+ raise TypeError(f'getconfigsetting, no fallback for "{func.__name__}"')
+
+ func = func or Config.get
+ return func(section=section, option=option, fallback=fallback)
+
+def get_natlink_system_config_filename() -> str:
+ return get_config_info_from_registry('installPath')
+
+def get_config_info_from_registry(key_name: str) -> str:
+ hive, key, flags = (winreg.HKEY_LOCAL_MACHINE, r'Software\Natlink', winreg.KEY_WOW64_32KEY)
+ with winreg.OpenKeyEx(hive, key, access=winreg.KEY_READ | flags) as natlink_key:
+ result, _ = winreg.QueryValueEx(natlink_key, key_name)
+ return result
+
+[docs]def config_locations() -> Iterable[str]:
+ """give two possible locations, the wanted and the "fallback" location
+
+ wanted: in the '.natlink' subdirectory of `home` or in "NATLINK_USERDIR".
+ name is always 'natlink.ini'
+
+ the fallback location is in the installed files, and provides the frame for the config file.
+ with the configurenatlink (natlinkconfigfunction.py or configfurenatlink.pyw) the fallback version
+ of the config file is copied into the wanted location.
+ """
+ join, expanduser, getenv, isfile = os.path.join, os.path.expanduser, os.getenv, os.path.isfile
+ home = expanduser('~')
+ config_sub_dir = '.natlink'
+ natlink_inifile = 'natlink.ini'
+ fallback_config_file = join(get_natlink_system_config_filename(), "DefaultConfig", natlink_inifile)
+ if not isfile(fallback_config_file):
+ raise OSError(f'fallback_config_file does not exist: "{fallback_config_file}"')
+ # try NATLINKUSERDIR setting:
+ natlink_userdir_from_env = getenv("NATLINK_USERDIR")
+ if natlink_userdir_from_env:
+ nl_user_dir = expand_path(natlink_userdir_from_env)
+ nl_user_file = join(nl_user_dir, natlink_inifile)
+ return [nl_user_file, fallback_config_file]
+
+ # choose between .natlink/natlink.ini in home or the fallback_directory:
+ return [join(home, config_sub_dir, natlink_inifile), fallback_config_file]
+
+def run() -> None:
+ logger = logging.getLogger('natlink')
+ try:
+ # TODO: remove this hack. As of October 2021, win32api does not load properly, except if
+ # the package pywin32_system32 is explictly put on new dll_directory white-list
+ pywin32_dir = os.path.join(sysconfig.get_path('platlib'), "pywin32_system32")
+ if os.path.isdir(pywin32_dir):
+ os.add_dll_directory(pywin32_dir)
+
+ config = NatlinkConfig.from_first_found_file(config_locations())
+ main = NatlinkMain(logger, config)
+ main.setup_logger()
+ main.start()
+ except Exception as exc:
+ print(f'Exception: "{exc}" in loader.run', file=sys.stderr)
+ print(traceback.format_exc())
+ raise Exception from exc
+
+if __name__ == "__main__":
+ natlink.natConnect()
+ run()
+ natlink.natDisconnect()
+
+
+#
+# natlinkstatus.py
+# This module gives the status of Natlink to natlinkmain
+#
+# (C) Copyright Quintijn Hoogenboom, February 2008/January 2018/extended for python3, Natlink5.0.1 Febr 2022
+#
+#pylint:disable=C0302, C0116, R0201, R0902, R0904, R0912, W0107, E1101
+"""The following functions are provided in this module:
+
+The functions below are put into the class NatlinkStatus.
+
+The functions below should not change anything in settings, only get information.
+
+getDNSInstallDir:
+ removed, not needed any more
+
+getDNSIniDir:
+ returns the directory where the NatSpeak INI files are located,
+ notably nssystem.ini and nsapps.ini. Got from loader.
+
+getDNSVersion:
+ returns the in the version number of NatSpeak, as an integer. So ..., 13, 15, ...
+ no distinction is made here between different subversions.
+ got indirectly from loader
+
+getWindowsVersion:
+ see source below
+
+get_language:
+ returns the 3 letter code of the language of the speech profile that
+ is open: 'enx', 'nld', "fra", "deu", "ita", "esp"
+
+ get it from loader (property), is updated when user profile changes (on_change_callback)
+ returns 'enx' when Dragon is not running.
+
+get_profile, get_user:
+ returns the directory of the current user profile information and
+ returns the name of the current user
+ This information is collected from natlink.getCurrentUser(), or from
+ the args in on_change_callback, with type == 'user'
+
+get_load_on_begin_utterance and set_load_on_begin_utterance:
+ returns value of this property of the natlinkmain (loader) instance.
+ True or False, or a (small) positive int, decreasing each utterance.
+
+ or
+ explicitly set this property.
+
+getPythonVersion:
+ return two character version, so without the dot! eg '38',
+
+ Note, no indication of 32 bit version, so no '38-32'
+
+
+getUserDirectory: get the Natlink user directory,
+ Especially Dragonfly users will use this directory for putting their grammar files in.
+ Also users that have their own custom grammar files can use this user directory
+
+getUnimacroDirectory: get the directory where the Unimacro system is.
+ When git cloned, relative to the Core directory, otherwise somewhere or in the site-packages (if pipped). This grammar will (and should) hold the _control.py grammar
+ and needs to be included in the load directories list of James' natlinkmain
+
+getUnimacroGrammarsDirectory: get the directory, where the user can put his Unimacro grammars. By default
+ this will be the ActiveGrammars subdirectory of the UnimacroUserDirectory.
+
+getUnimacroUserDirectory: get the directory of Unimacro INI files, if not return '' or
+ the Unimacro user directory
+
+getVocolaDirectory: get the directory where the Vocola system is. When cloned from git, in Vocola, relative to
+ the Core directory. Otherwise (when pipped) in some site-packages directory. It holds (and should hold) the
+ grammar _vocola_main.py.
+
+getVocolaUserDirectory: get the directory of Vocola User files, if not return ''
+ (if run from natlinkconfigfunctions use getVocolaDirectoryFromIni, which checks inifile
+ at each call...)
+
+getVocolaGrammarsDirectory: get the directory, where the compiled Vocola grammars are/will be.
+ This will normally be the "CompiledGrammars" subdirectory of the VocolaUserDirectory.
+
+NatlinkIsEnabled:
+ return 1 or 0 whether Natlink is enabled or not
+ returns None when strange values are found
+ (checked with the INI file settings of NSSystemIni and NSAppsIni)
+
+getVocolaTakesLanguages: additional settings for Vocola
+
+new 2014/2022
+getDNSName: return "NatSpeak" for versions <= 11 and "Dragon" for 12 (on) (obsolete in 2022)
+getAhkExeDir: return the directory where AutoHotkey is found (only needed when not in default)
+getAhkUserDir: return User Directory of AutoHotkey, not needed when it is in default.
+get_language and other properties, see above.
+
+"""
+import os
+import sys
+import stat
+import platform
+import logging
+from typing import Any
+try:
+ from natlink import loader
+except ModuleNotFoundError:
+ print('Natlink is not enabled, module natlink and/or natlink.loader cannot be found\n\texit natlinkstatus.py...')
+ sys.exit()
+from natlink import config
+from natlink import singleton
+import natlink
+
+## setup a natlinkmain instance, for getting properties from the loader:
+## note, when loading the natlink module via Dragon, you can call simply:
+# # # natlinkmain = loader.NatlinkMain()
+
+## setting up Logger and Config is needed, when running this for test:
+Logger = logging.getLogger('natlink')
+Config = config.NatlinkConfig.from_first_found_file(loader.config_locations())
+natlinkmain = loader.NatlinkMain(Logger, Config)
+
+# the possible languages (for get_language), now in loader
+
+shiftKeyDict = {"nld": "Shift",
+ "enx": 'shift',
+ "fra": "maj",
+ "deu": "umschalt",
+ "ita": "maiusc",
+ "esp": "may\xfas"}
+
+thisDir, thisFile = os.path.split(__file__)
+
+[docs]class NatlinkStatus(metaclass=singleton.Singleton):
+ """this class holds the Natlink status functions.
+
+ This class is a Singleton, which means that all instances are the same object.
+
+ Some information is retrieved from the loader, the natlinkmain (Singleton) instance.
+
+ In natlinkconfigfunctions.py, NatlinkStatus is subclassed for configuration purposes.
+ in the PyTest folder there are/come test functions in TestNatlinkStatus
+
+ """
+ known_directory_options = ['userdirectory', 'dragonflyuserdirectory',
+ 'unimacrodirectory', 'unimacrogrammarsdirectory',
+ 'vocoladirectory', 'vocolagrammarsdirectory']
+
+ def __init__(self):
+ """initialise all instance variables, in this singleton class, hoeinstance
+ """
+ self.natlinkmain = natlinkmain # global
+ self.DNSVersion = None
+ self.DNSIniDir = None
+ self.CoreDirectory = None
+ self.NatlinkDirectory = None
+ self.UserDirectory = None
+ ## Unimacro:
+ self.UnimacroDirectory = None
+ self.UnimacroUserDirectory = None
+ self.UnimacroGrammarsDirectory = None
+ ## Vocola:
+ self.VocolaUserDirectory = None
+ self.VocolaDirectory = None
+ self.VocolaGrammarsDirectory = None
+ ## AutoHotkey:
+ self.AhkUserDir = None
+ self.AhkExeDir = None
+
+ if self.CoreDirectory is None:
+ self.CoreDirectory = thisDir
+
+[docs] @staticmethod
+ def getWindowsVersion():
+ """extract the windows version
+
+ return 1 of the predefined values above, or just return what the system
+ call returns
+ """
+ wVersion = platform.platform()
+ if '-' in wVersion:
+ return wVersion.split('-')[1]
+ print('Warning, probably cannot find correct Windows Version... (%s)'% wVersion)
+ return wVersion
+
+[docs] def getPythonVersion(self):
+ """get the version of python
+
+ Check if the version is supported on the "lower" side.
+
+ length 2, without ".", so "38" etc.
+ """
+ version = sys.version[:3]
+ version = version.replace(".", "")
+ return version
+
+ @property
+ def user(self) -> str:
+ return self.natlinkmain.user
+ @property
+ def profile(self) -> str:
+ return self.natlinkmain.profile
+ @property
+ def language(self) -> str:
+ return self.natlinkmain.language
+
+ @property
+ def load_on_begin_utterance(self) -> Any:
+ """inspect current value of this loader setting
+ """
+ return self.natlinkmain.load_on_begin_utterance
+
+ def get_user(self):
+ return self.user
+
+ def get_profile(self):
+ return self.profile
+
+ def get_language(self):
+ return self.language
+
+ def get_load_on_begin_utterance(self):
+ return self.load_on_begin_utterance
+
+[docs] def getDNSIniDir(self):
+ """get the path (one above the users profile paths) where the INI files
+ should be located
+
+ """
+ # first try if set (by configure dialog/natlinkinstallfunctions.py) if regkey is set:
+ if self.DNSIniDir is not None:
+ return self.DNSIniDir
+
+ self.DNSIniDir = loader.get_config_info_from_registry("dragonIniDir")
+ return self.DNSIniDir
+
+
+[docs] def getDNSVersion(self):
+ """find the correct DNS version number (as an integer)
+
+ 2022: extract from the dragonIniDir setting in the registry, via loader function
+
+ """
+ if self.DNSVersion is not None:
+ return self.DNSVersion
+ dragonIniDir = loader.get_config_info_from_registry("dragonIniDir")
+ if dragonIniDir:
+ try:
+ version = int(dragonIniDir[-2:])
+ except ValueError:
+ print('getDNSVersion, invalid version found "{dragonIniDir[-2:]}", return 0')
+ version = 0
+ else:
+ print(f'Error, cannot get dragonIniDir from registry, unknown DNSVersion "{dragonIniDir}", return 0')
+ version = 0
+ self.DNSVersion = version
+ return self.DNSVersion
+
+[docs] def VocolaIsEnabled(self):
+ """Return True if Vocola is enables
+
+ To be so,
+ 1. the VocolaUserDirectory (where the vocola command files (.vcl) are located)
+ should be defined in the user config file
+ 2. the VocolaDirectory should be found, and hold '_vocola_main.py'
+
+ """
+ isdir = os.path.isdir
+ vocUserDir = self.getVocolaUserDirectory()
+ if vocUserDir and isdir(vocUserDir):
+ vocDir = self.getVocolaDirectory()
+ vocGrammarsDir = self.getVocolaGrammarsDirectory()
+ if vocDir and isdir(vocDir) and vocGrammarsDir and isdir(vocGrammarsDir):
+ return True
+ return False
+
+
+[docs] def UnimacroIsEnabled(self):
+ """UnimacroIsEnabled: see if UnimacroDirectory and UnimacroUserDirectory are there
+
+ _control.py should be in the UnimacroDirectory.
+ """
+ isdir = os.path.isdir
+ uDir = self.getUnimacroDirectory()
+ if not uDir:
+ # print('no valid UnimacroDirectory, Unimacro is disabled')
+ return False
+ if isdir(uDir):
+ files = os.listdir(uDir)
+ if not '_control.py' in files:
+ print(f'UnimacroDirectory is present ({uDir}), but not "_control.py" grammar file')
+ return False # _control.py should be in Unimacro directory
+
+ uuDir = self.getUnimacroUserDirectory()
+ if not uuDir:
+ return False
+ ugDir = self.getUnimacroGrammarsDirectory()
+ if not (ugDir and isdir(ugDir)):
+ print(f'UnimacroGrammarsDirectory ({ugDir}) not present, please create')
+ return False
+ return True
+
+
+ def UserIsEnabled(self):
+ userDir = self.getUserDirectory()
+ if userDir:
+ return True
+ return False
+
+ def getUnimacroUserDirectory(self):
+ isdir, normpath = os.path.isdir, os.path.normpath
+ if self.UnimacroUserDirectory is not None:
+ return self.UnimacroUserDirectory
+ key = 'unimacrouserdirectory'
+ value = self.natlinkmain.getconfigsetting(section="unimacro", option=key)
+ if value and isdir(value):
+ self.UnimacroUserDirectory = value
+ return normpath(value)
+ if value:
+ expanded = loader.expand_path(value)
+ if expanded and isdir(expanded):
+ self.UnimacroUserDirectory = expanded
+ return normpath(expanded)
+ print(f'invalid directory for "{key}": "{value}"')
+ self.UnimacroUserDirectory = ''
+ return ''
+
+
+[docs] def getUnimacroDirectory(self):
+ """return the path to the UnimacroDirectory
+
+ This is the directory where the _control.py grammar is, and
+ is normally got via `pip install unimacro`
+
+ """
+ # When git cloned, relative to the Core directory, otherwise somewhere or in the site-packages (if pipped).
+ join, isdir, isfile, normpath = os.path.join, os.path.isdir, os.path.isfile, os.path.normpath
+ if self.UnimacroDirectory is not None:
+ return self.UnimacroDirectory
+ uDir = join(sys.prefix, "lib", "site-packages", "unimacro")
+ if isdir(uDir):
+ uFile = "_control.py"
+ controlGrammar = join(uDir, uFile)
+ if isfile(controlGrammar):
+ self.UnimacroDirectory = normpath(uDir)
+ return self.UnimacroDirectory
+ # print(f'UnimacroDirectory found: "{uDir}", but no valid file: "{uFile}", return ""')
+ return ""
+ # print('UnimacroDirectory not found in "lib/site-packages/unimacro", return ""')
+ self.UnimacroDirectory = ""
+ return ""
+
+
+[docs] def getUnimacroGrammarsDirectory(self):
+ """return the path to the directory where the ActiveGrammars of Unimacro are located.
+
+ Expected in "ActiveGrammars" of the UnimacroUserDirectory
+ (August 2020)
+
+ """
+ isdir, join, normpath, listdir = os.path.isdir, os.path.join, os.path.normpath, os.listdir
+ if self.UnimacroGrammarsDirectory is not None:
+ return self.UnimacroGrammarsDirectory
+
+ uDir = self.getUnimacroDirectory()
+ if not uDir:
+ self.UnimacroGrammarsDirectory = ''
+ return ''
+
+ uuDir = self.getUnimacroUserDirectory()
+ if uuDir and isdir(uuDir):
+ ugDir = join(uuDir, "ActiveGrammars")
+ if not isdir(ugDir):
+ os.mkdir(ugDir)
+ if isdir(ugDir):
+ ugFiles = [f for f in listdir(ugDir) if f.endswith(".py")]
+ if not ugFiles:
+ print(f"UnimacroGrammarsDirectory: {ugDir} has no python grammar files (yet), please populate this directory with the Unimacro grammars you wish to use, and then toggle your microphone")
+
+ try:
+ del self.UnimacroGrammarsDirectory
+ except AttributeError:
+ pass
+ self.UnimacroGrammarsDirectory= normpath(ugDir)
+ return self.UnimacroGrammarsDirectory
+
+ try:
+ del self.UnimacroGrammarsDirectory
+ except AttributeError:
+ pass
+ self.UnimacroGrammarsDirectory= "" # meaning is not set, for future calls.
+ return self.UnimacroGrammarsDirectory
+
+
+[docs] def getCoreDirectory(self):
+ """return the path of the coreDirectory, MacroSystem/core
+ """
+ return self.CoreDirectory
+
+
+[docs] def getNatlinkDirectory(self):
+ """return the path of the NatlinkDirectory, two above the coreDirectory
+ """
+ return self.NatlinkDirectory
+
+
+[docs] def getUserDirectory(self):
+ """return the path to the Natlink User directory
+
+ this one is not any more for Unimacro, but for User specified grammars, also Dragonfly
+
+ should be set in configurenatlink, otherwise ignore...
+ """
+ isdir, normpath = os.path.isdir, os.path.normpath
+ if not self.UserDirectory is None:
+ return self.UserDirectory
+ key = 'UserDirectory'
+ value = self.natlinkmain.getconfigsetting(section='directories', option=key)
+ if value and isdir(value):
+ self.UserDirectory = normpath(value)
+ return self.UserDirectory
+ expanded = config.expand_path(value)
+ if expanded and isdir(expanded):
+ self.UserDirectory = normpath(expanded)
+ return self.UserDirectory
+
+ print('invalid path for UserDirectory: "%s"'% value)
+ self.UserDirectory = ''
+ return ''
+
+
+ def getVocolaUserDirectory(self):
+
+ isdir, normpath = os.path.isdir, os.path.normpath
+ if self.VocolaUserDirectory is not None:
+ return self.VocolaUserDirectory
+ key = 'vocolauserdirectory'
+ section = 'vocola'
+ value = self.natlinkmain.getconfigsetting(section=section, option=key)
+ if value and isdir(value):
+ self.VocolaUserDirectory = normpath(value)
+ return value
+ expanded = config.expand_path(value)
+ if expanded and isdir(expanded):
+ self.VocolaUserDirectory = normpath(expanded)
+ return self.VocolaUserDirectory
+
+ print(f'invalid path for VocolaUserDirectory: "{value}"')
+ self.VocolaUserDirectory = ''
+ return ''
+
+ def getVocolaDirectory(self):
+ isdir, isfile, join, normpath = os.path.isdir, os.path.isfile, os.path.join, os.path.normpath
+ if self.VocolaDirectory is not None:
+ return self.VocolaDirectory
+
+ ## try in site-packages:
+ vocDir = join(sys.prefix, "lib", "site-packages", "vocola2")
+ if not isdir(vocDir):
+ # print('VocolaDirectory not found in "lib/site-packages/vocola2", return ""')
+ self.VocolaDirectory = ''
+ return ''
+ vocFile = "_vocola_main.py"
+ checkGrammar = join(vocDir, vocFile)
+ if not isfile(checkGrammar):
+ print(f'VocolaDirectory found in "{vocDir}", but no file "{vocFile}" found, return ""')
+ self.VocolaDirectory = ''
+ return ''
+
+ self.VocolaDirectory = normpath(vocDir)
+ return self.VocolaDirectory
+
+
+[docs] def getVocolaGrammarsDirectory(self):
+ """return the VocolaGrammarsDirectory, but only if Vocola is enabled
+
+ If so, the subdirectory CompiledGrammars is created if not there yet.
+
+ The path of this "CompiledGrammars" directory is returned.
+
+ If Vocola is not enabled, or anything goes wrong, return ""
+
+ """
+ join, normpath = os.path.join, os.path.normpath
+ if self.VocolaGrammarsDirectory is not None:
+ return self.VocolaGrammarsDirectory
+
+ vUserDir = self.getVocolaUserDirectory()
+ if not vUserDir:
+ self.VocolaGrammarsDirectory = ''
+ return ''
+
+ vgDir = join(vUserDir, 'VocolaGrammars')
+ self.VocolaGrammarsDirectory = normpath(vgDir)
+ return self.VocolaGrammarsDirectory
+
+
+ def getAhkUserDir(self):
+ if not self.AhkUserDir is None:
+ return self.AhkUserDir
+ return self.getAhkUserDirFromIni()
+
+
+ def getAhkUserDirFromIni(self):
+ isdir, normpath = os.path.isdir, os.path.normpath
+ key = 'AhkUserDir'
+ value = self.natlinkmain.getconfigsetting(section='autohotkey', option=key)
+ if value and isdir(value):
+ self.AhkUserDir = normpath(value)
+ return value
+ expanded = config.expand_path(value)
+ if expanded and isdir(expanded):
+ self.AhkUserDir= normpath(expanded)
+ return self.AhkUserDir
+
+ print(f'invalid path for AhkUserDir: "{value}", return ""')
+ self.AhkUserDir = ''
+ return ''
+
+
+ def getAhkExeDir(self):
+ if not self.AhkExeDir is None:
+ return self.AhkExeDir
+ return self.getAhkExeDirFromIni()
+
+
+ def getAhkExeDirFromIni(self):
+ isdir, normpath = os.path.isdir, os.path.normpath
+ key = 'AhkExeDir'
+ value = self.natlinkmain.getconfigsetting(section='autohotkey', option=key)
+ if value and isdir(value):
+ self.AhkExeDir = normpath(value)
+ return value
+ expanded = config.expand_path(value)
+ if expanded and isdir(expanded):
+ self.AhkExeDir = normpath(expanded)
+ return self.AhkExeDir
+
+ print(f'invalid path for AhkExeDir: "{value}", return ""')
+ self.AhkExeDir = ''
+ return ''
+
+[docs] def getExtraGrammarDirectories(self):
+ """record grammar directories that are unknown to natlinkstatus and natlinkconfigfunctions
+
+ These directories can be entered "manually" in the `natlink.ini` file
+ """
+ result = self.natlinkmain.getconfigsetting(section='directories')
+ return [s for s in result if s not in self.known_directory_options]
+
+ def getUnimacroIniFilesEditor(self):
+ key = 'UnimacroIniFilesEditor'
+ value = self.natlinkmain.getconfigsetting(section='unimacro', option=key)
+ if not value:
+ value = 'notepad'
+ if self.UnimacroIsEnabled():
+ return value
+ return ''
+
+[docs] def getShiftKey(self):
+ """return the shiftkey, for setting in natlinkmain when user language changes.
+
+ used for self.playString in natlinkutils, for the dropping character bug. (dec 2015, QH).
+ """
+ ## TODO: must be windows language...
+ windowsLanguage = 'enx' ### ??? TODO QH
+ try:
+ return "{%s}"% shiftKeyDict[windowsLanguage]
+ except KeyError:
+ print(f'no shiftKey code provided for language: "{windowsLanguage}", take empty string.')
+ return ""
+
+ # get additional options Vocola
+
+[docs] def getVocolaTakesLanguages(self):
+ """gets and value for distinction of different languages in Vocola
+ If Vocola is not enabled, this option will also return False
+ """
+ key = 'vocola_takes_languages'
+ return self.natlinkmain.getconfigsetting(section="vocola", option=key, func='getboolean')
+
+[docs] def getVocolaTakesUnimacroActions(self):
+ """gets and value for optional Vocola takes Unimacro actions
+ If Vocola is not enabled, this option will also return False
+ """
+ key = 'VocolaTakesUnimacroActions'
+ return self.natlinkmain.getconfigsetting(section="vocola", option=key, func='getboolean')
+
+
+ def getInstallVersion(self):
+ version = loader.get_config_info_from_registry("version")
+ return version
+
+[docs] @staticmethod
+ def getDNSName():
+ """return NatSpeak for versions <= 11, and Dragon for versions >= 12
+ """
+ return "Dragon"
+
+
+[docs] def getNatlinkStatusDict(self):
+ """return actual status in a dict
+
+ Most values come via properties...
+
+ """
+ D = {}
+ # properties:
+ D['user'] = self.get_user()
+ D['profile'] = self.get_profile()
+ D['language'] = self.get_language()
+ D['load_on_begin_utterance'] = self.get_load_on_begin_utterance()
+
+ for key in ['DNSIniDir', 'WindowsVersion', 'DNSVersion',
+ 'PythonVersion',
+ 'DNSName',
+ 'UnimacroDirectory', 'UnimacroUserDirectory', 'UnimacroGrammarsDirectory',
+ 'VocolaDirectory', 'VocolaUserDirectory', 'VocolaGrammarsDirectory',
+ 'VocolaTakesLanguages', 'VocolaTakesUnimacroActions',
+ 'UnimacroIniFilesEditor',
+ 'ExtraGrammarDirectories',
+ 'InstallVersion',
+ # 'IncludeUnimacroInPythonPath',
+ 'AhkExeDir', 'AhkUserDir']:
+## 'BaseTopic', 'BaseModel']:
+ func_name = f'get{key[0].upper() + key[1:]}'
+ func = getattr(self, func_name, None)
+ if func:
+ D[key] = func()
+ else:
+ print(f'no valid function for getting key: "{key}" ("{func_name}")')
+
+ D['CoreDirectory'] = self.CoreDirectory
+ D['UserDirectory'] = self.getUserDirectory()
+ D['vocolaIsEnabled'] = self.VocolaIsEnabled()
+
+ D['unimacroIsEnabled'] = self.UnimacroIsEnabled()
+ D['userIsEnabled'] = self.UserIsEnabled()
+ # extra for information purposes:
+ D['NatlinkDirectory'] = self.NatlinkDirectory
+ return D
+
+
+ def getNatlinkStatusString(self):
+ L = []
+ D = self.getNatlinkStatusDict()
+ L.append('--- properties:')
+ self.appendAndRemove(L, D, 'user')
+ self.appendAndRemove(L, D, 'profile')
+ self.appendAndRemove(L, D, 'language')
+ self.appendAndRemove(L, D, 'load_on_begin_utterance')
+
+ # Natlink::
+ L.append('')
+ key = 'CoreDirectory'
+ self.appendAndRemove(L, D, key)
+ key = 'InstallVersion'
+ self.appendAndRemove(L, D, key)
+
+ ## Vocola::
+ if D['vocolaIsEnabled']:
+ self.appendAndRemove(L, D, 'vocolaIsEnabled', "---Vocola is enabled")
+ for key in ('VocolaUserDirectory', 'VocolaDirectory',
+ 'VocolaGrammarsDirectory', 'VocolaTakesLanguages',
+ 'VocolaTakesUnimacroActions'):
+ self.appendAndRemove(L, D, key)
+ else:
+ self.appendAndRemove(L, D, 'vocolaIsEnabled', "---Vocola is disabled")
+ for key in ('VocolaUserDirectory', 'VocolaDirectory',
+ 'VocolaGrammarsDirectory', 'VocolaTakesLanguages',
+ 'VocolaTakesUnimacroActions'):
+ del D[key]
+
+ ## Unimacro:
+ if D['unimacroIsEnabled']:
+ self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is enabled")
+ for key in ('UnimacroUserDirectory', 'UnimacroDirectory', 'UnimacroGrammarsDirectory'):
+ self.appendAndRemove(L, D, key)
+ for key in ('UnimacroIniFilesEditor',):
+ self.appendAndRemove(L, D, key)
+ else:
+ self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is disabled")
+ for key in ('UnimacroUserDirectory', 'UnimacroIniFilesEditor',
+ 'UnimacroDirectory', 'UnimacroGrammarsDirectory'):
+ del D[key]
+ ## UserDirectory:
+ if D['userIsEnabled']:
+ self.appendAndRemove(L, D, 'userIsEnabled', "---User defined grammars are enabled")
+ for key in ('UserDirectory',):
+ self.appendAndRemove(L, D, key)
+ else:
+ self.appendAndRemove(L, D, 'userIsEnabled', "---User defined grammars are disabled")
+ del D['UserDirectory']
+
+ ## remaining Natlink options:
+ L.append('other Natlink info:')
+
+ # system:
+ L.append('system information:')
+ for key in ['DNSIniDir', 'DNSVersion', 'DNSName',
+ 'WindowsVersion', 'PythonVersion']:
+ self.appendAndRemove(L, D, key)
+
+ # forgotten???
+ if D:
+ L.append('remaining information:')
+ for key in list(D.keys()):
+ self.appendAndRemove(L, D, key)
+
+ return '\n'.join(L)
+
+
+ def appendAndRemove(self, List, Dict, Key, text=None):
+ if text:
+ List.append(text)
+ else:
+ value = Dict[Key]
+ if value is None or value == '':
+ value = '-'
+ if len(Key) <= 6:
+ List.append(f'\t{Key}\t\t\t{value}')
+ elif len(Key) <= 13:
+ List.append(f'\t{Key}\t\t{value}')
+ else:
+ List.append(f'\t{Key}\t{value}')
+ del Dict[Key]
+
+def getFileDate(modName):
+ #pylint:disable=C0321
+ try: return os.stat(modName)[stat.ST_MTIME]
+ except OSError: return 0 # file not found
+
+def main():
+ status = NatlinkStatus()
+
+ Lang = status.get_language()
+ print(f'language: "{Lang}"')
+ print(status.getNatlinkStatusString())
+ print(f'load_on_begin_utterance: {status.get_load_on_begin_utterance()}')
+ dns_version = status.getDNSVersion()
+ print(f'DNSVersion: {dns_version} (type: {type(dns_version)})')
+
+if __name__ == "__main__":
+ natlink.natConnect()
+ main()
+ natlink.natDisconnect()
+