diff --git a/src/natlinkcore/__init__.py b/src/natlinkcore/__init__.py index 3837cca..cb37768 100644 --- a/src/natlinkcore/__init__.py +++ b/src/natlinkcore/__init__.py @@ -1,6 +1,6 @@ '''Python portion of Natlink, a compatibility module for Dragon Naturally Speaking The python stuff including test modules''' -__version__="5.3.5" +__version__="5.3.6" #pylint:disable= from pathlib import Path diff --git a/src/natlinkcore/loader.py b/src/natlinkcore/loader.py index bdeb881..7717afe 100644 --- a/src/natlinkcore/loader.py +++ b/src/natlinkcore/loader.py @@ -260,9 +260,12 @@ def _call_and_catch_all_exceptions(self, fn: Callable[[], None]) -> None: 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) + if unload is None: + self.logger.info(f'cannot unload module {module.__name__}') + return + 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: @@ -357,6 +360,13 @@ def load_or_reload_modules(self, mod_paths: Iterable[Path], force_load: bool = N for mod_path in mod_paths: self.load_or_reload_module(mod_path, force_load=force_load) self.seen.add(mod_path) + + def unload_all_loaded_modules(self): + """unload the modules that are loaded, and empty the bad modules list + """ + for module in self.loaded_modules.values(): + self.unload_module(module) + self.bad_modules.clear() def remove_modules_that_no_longer_exist(self) -> None: mod_paths = self.module_paths_for_user @@ -401,6 +411,8 @@ def on_change_callback(self, change_type: str, args: Any) -> None: 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: + # added line, QH, 2023-10-08 + self.unload_all_loaded_modules() self.trigger_load(force_load=True) elif change_type == 'mic' and args == 'on': self.logger.debug('on_change_callback called with: "mic", "on"') @@ -487,10 +499,9 @@ def set_user_language(self, args: Any = None): 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.logger.warning('set_user_language, cannot get input for get_user_language, set to "enx",\n\tprobably Dragon is not running or you are preforming pytests') 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 diff --git a/src/natlinkcore/natlinkstatus.py b/src/natlinkcore/natlinkstatus.py index d5565ab..816fe14 100644 --- a/src/natlinkcore/natlinkstatus.py +++ b/src/natlinkcore/natlinkstatus.py @@ -135,7 +135,7 @@ class NatlinkStatus(metaclass=singleton.Singleton): """ known_directory_options = ['userdirectory', 'dragonflyuserdirectory', - 'unimacrodirectory', 'unimacrogrammarsdirectory', + 'unimacrodirectory', 'vocoladirectory', 'vocolagrammarsdirectory'] def __init__(self): @@ -152,7 +152,7 @@ def __init__(self): ## Unimacro: self.UnimacroDirectory = None self.UnimacroUserDirectory = None - self.UnimacroGrammarsDirectory = None + # self.UnimacroGrammarsDirectory = None self.UnimacroDataDirectory = None ## Vocola: self.VocolaUserDirectory = None @@ -426,34 +426,34 @@ def getUnimacroDataDirectory(self): return um_data_dir - def getUnimacroGrammarsDirectory(self): - """return the path to the directory where (part of) the ActiveGrammars of Unimacro are located. - - By default in the UnimacroGrammars subdirectory of site-packages/unimacro, but look in natlink.ini file... - - """ - isdir, abspath = os.path.isdir, os.path.abspath - if self.UnimacroGrammarsDirectory is not None: - return self.UnimacroGrammarsDirectory - key = 'unimacrogrammarsdirectory' - value = self.natlinkmain.getconfigsetting(section="directories", option=key) - if not value: - self.UnimacroGrammarsDirectory = '' - return '' - if isdir(value): - self.UnimacroGrammarDirectory = value - return abspath(value) - - expanded = config.expand_path(value) - if expanded and isdir(expanded): - self.UnimacroGrammarDirectory = abspath(expanded) - return self.UnimacroGrammarDirectory - - # check_natlinkini = - self.UnimacroGrammarsDirectory = '' - - return '' - + # def getUnimacroGrammarsDirectory(self): + # """return the path to the directory where (part of) the ActiveGrammars of Unimacro are located. + # + # By default in the UnimacroGrammars subdirectory of site-packages/unimacro, but look in natlink.ini file... + # + # """ + # isdir, abspath = os.path.isdir, os.path.abspath + # if self.UnimacroGrammarsDirectory is not None: + # return self.UnimacroGrammarsDirectory + # key = 'unimacrogrammarsdirectory' + # value = self.natlinkmain.getconfigsetting(section="directories", option=key) + # if not value: + # self.UnimacroGrammarsDirectory = '' + # return '' + # if isdir(value): + # self.UnimacroGrammarDirectory = value + # return abspath(value) + # + # expanded = config.expand_path(value) + # if expanded and isdir(expanded): + # self.UnimacroGrammarDirectory = abspath(expanded) + # return self.UnimacroGrammarDirectory + # + # # check_natlinkini = + # self.UnimacroGrammarsDirectory = '' + # + # return '' + # def getNatlinkDirectory(self): """return the path of the NatlinkDirectory, where the _natlink_core.pyd package (C++ code) is """ @@ -807,7 +807,7 @@ def getNatlinkStatusString(self): ## Unimacro: if D['unimacroIsEnabled']: self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is enabled") - for key in ('UnimacroUserDirectory', 'UnimacroDirectory', 'UnimacroGrammarsDirectory', 'UnimacroDataDirectory'): + for key in ('UnimacroUserDirectory', 'UnimacroDirectory', 'UnimacroDataDirectory'): self.appendAndRemove(L, D, key) else: self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is disabled") diff --git a/src/natlinkcore/nsformat.py b/src/natlinkcore/nsformat.py index 5b4da1b..f7bbab2 100644 --- a/src/natlinkcore/nsformat.py +++ b/src/natlinkcore/nsformat.py @@ -9,7 +9,7 @@ removed pre 11 things, now for python3 version, with (normally) DNSVersion 15 (QH, June 2020)/Febr 2022 """ -#pylint:disable=C0116, C0123, R0911, R0912, R0915, R0916 +#pylint:disable=C0116, C0123, R0911, R0912, R0915, R0916, R1735, R1728 import copy import natlink @@ -51,37 +51,37 @@ if name.startswith('flag_') and isinstance(globals()[name], int) and 0 < globals()[name] < 32: flagNames[globals()[name]] = name # -flags_like_period = (9, 4, 21, 17) # flag_two_spaces_next = 9, flag_passive_cap_next = 4, flag_no_space_before = 21 -flags_like_comma = (21, ) # flag_no_space_before = 21 (flag_nodelete = 3 we just ignore here, so leave out) -flags_like_number = (10,) -flags_like_point = (8, 10, 21) # no spacing (combination with numbers seems +flags_like_period = {9, 4, 21, 17} # flag_two_spaces_next = 9, flag_passive_cap_next = 4, flag_no_space_before = 21 +flags_like_comma = {21} # flag_no_space_before = 21 (flag_nodelete = 3 we just ignore here, so leave out) +flags_like_number = {10} +flags_like_point = {8, 10, 21} # no spacing (combination with numbers seems # obsolete (cond_no_space = 10) -flags_like_hyphen = (8, 21) # no spacing before and after -flags_like_open_quote = (8, 20) # no space next and no cap change -flags_like_close_quote = (21, 20, 19) # no space before, no cap change and no space change (??) +flags_like_hyphen = {8, 21} # no spacing before and after +flags_like_open_quote = {8, 20} # no space next and no cap change +flags_like_close_quote = {21, 20, 19} # no space before, no cap change and no space change (??) # word flags from properties part of the word: # Dragon 11... propDict = {} -propDict['space-bar'] = (flag_space_bar, flag_no_space_next, flag_no_formatting, - flag_no_cap_change, flag_no_space_before) # (8, 18, 20, 21, 27) +propDict['space-bar'] = {flag_space_bar, flag_no_space_next, flag_no_formatting, + flag_no_cap_change, flag_no_space_before} # {8, 18, 20, 21, 27} propDict['period'] = flags_like_period propDict['point'] = flags_like_point propDict['dot'] = flags_like_point propDict['comma'] = flags_like_comma propDict['cap'] = (19, 18, flag_active_cap_next) -propDict['caps-on'] = (19, 18, flag_cap_all) -propDict['caps-off'] = (19, 18, flag_reset_uc_lc_caps) -propDict['all-caps'] = (19, 18, flag_uppercase_next) -propDict['all-caps-on'] = (19, 18, flag_uppercase_all) -propDict['all-caps-off'] = (19, 18, flag_reset_uc_lc_caps) -propDict['no-caps'] = (19, 18, flag_lowercase_next) -propDict['no-caps-on'] = (19, 18, flag_lowercase_all) -propDict['no-caps-off'] = (19, 18, flag_reset_uc_lc_caps) -propDict['no-space'] = (18, 20, flag_no_space_next) -propDict['no-space-on'] = (18, 20, flag_no_space_all) -propDict['no-space-off'] = (18, 20, flag_reset_no_space) +propDict['caps-on'] = {19, 18, flag_cap_all} +propDict['caps-off'] = {19, 18, flag_reset_uc_lc_caps} +propDict['all-caps'] = {19, 18, flag_uppercase_next} +propDict['all-caps-on'] = {19, 18, flag_uppercase_all} +propDict['all-caps-off'] = {19, 18, flag_reset_uc_lc_caps} +propDict['no-caps'] = {19, 18, flag_lowercase_next} +propDict['no-caps-on'] = {19, 18, flag_lowercase_all} +propDict['no-caps-off'] = {19, 18, flag_reset_uc_lc_caps} +propDict['no-space'] = {18, 20, flag_no_space_next} +propDict['no-space-on'] = {18, 20, flag_no_space_all} +propDict['no-space-off'] = {18, 20, flag_reset_no_space} propDict['left-double-quote'] = flags_like_open_quote propDict['right-double-quote'] = flags_like_close_quote # left- as left-double-quote @@ -112,15 +112,17 @@ # # If you already have the wordInfo for each word, you can pass in a list of # tuples of (wordName,wordInfo) instead of just the list of words. - -def formatWords(wordList,state=None): +def formatWords(wordList, state=None): """return the formatted words and the state at end. when passing this state in the next call, the spacing and capitalization will be maintained. + """ #pylint:disable=W0603 global flags_like_period + assert isinstance(wordList, list) + language = 'enx' if language != 'enx': flags_like_period = (4, 21, 17) # one space after period. @@ -164,6 +166,41 @@ def formatWords(wordList,state=None): return output, state + +def formatString(text, state=None): + r"""pass in a string, and optionally the result of previous call + + Do NOT require the .\period\period specifications, so you can call this one from a plain sentence. + + + For frequent punctuation, like "." these are replaced by the counterparts. + """ + like_period = ".?!" + like_comma = ",;:" + like_hyphen = "-" + + all_special = like_period + like_comma + like_hyphen + for char in all_special: + text = text.replace(char, f' {char} ') + + input_list = text.split() + for i, w in enumerate(input_list): + if w not in all_special: + continue + if w in like_period: + input_list[i] = (w, set(flags_like_period)) + if w in like_comma: + input_list[i] = (w, set(flags_like_comma)) + if w in like_hyphen: + input_list[i] = (w, set(flags_like_hyphen)) + + + + result, state = formatWords(input_list, state=state) + return result, state + + + countDict= dict(one=1, two=2, three=3, four=4, five=5, six=6, seven=7, eight=8, nine=9, een=1, twee=2, drie=3, vier=4, vijf=5, zes=6, zeven=7, acht=8, negen=9) @@ -236,7 +273,7 @@ def formatWord(wordName,wordInfo=None,stateFlags=None, gwi=None): elif isinstance(state, (list, tuple)): state = set(state) else: - raise ValueError("formatWord, invalid stateFlags: %s"% repr(stateFlags)) + raise ValueError(f'formatWord, invalid stateFlags: {repr(stateFlags)}') stateFlags = copy.copy(state) @@ -392,7 +429,7 @@ def getWordInfo(word): return set(propDict['left-double-quote']) if prop.startswith('right-'): return set(propDict['right-double-quote']) - print('getWordInfo11, unknown word property: "%s" ("%s")'% (prop, word)) + print(f'getWordInfo, unknown word property: {prop} {"word"}') return set() # empty tuple # should not come here return set() @@ -445,8 +482,8 @@ def testSubroutine(state, Input, Output): words[i] = words[i].replace('_', ' ') actual,state = formatWords(words,state) if actual != Output: - print('Expected "%s"'%Output) - print('Actually "%s"'%actual) + print(f'Expected {"Output"}') + print(f'Actually {"actual"}') raise ValueError("test error") return state diff --git a/tests/test_loader.py b/tests/test_loader.py index 0e8d046..05a22ea 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,8 +1,9 @@ #pylint:disable= C0114, C0116, W0401, W0614, W0621, W0108. W0212, C2801, C3001 +from pathlib import Path + import pytest -import pathlib as p from natlinkcore.loader import * import debugpy @@ -35,7 +36,7 @@ def sample_config(sample_name) -> 'NatlinkConfig': """ load a config file from the config files subfolder """ - sample_ini= (p.Path(__file__).parent) / "config_files" / sample_name + sample_ini= (Path(__file__).parent) / "config_files" / sample_name test_config = NatlinkConfig.from_file(sample_ini) return test_config @@ -213,7 +214,7 @@ def test_load_single_good_script_from_user_dir(tmpdir, empty_config, logger, mon assert set(main.load_attempt_times.keys()) == {a_path} assert main.load_attempt_times[a_path] == mtime assert main.loaded_modules[a_path].x == 0 - + assert logger.messages['info'][0] == 'loading module: _a' del_loaded_modules(main) @@ -242,7 +243,7 @@ def test_reload_single_changed_good_script(tmpdir, empty_config, logger, monkeyp assert set(main.load_attempt_times.keys()) == {a_path} assert main.load_attempt_times[a_path] == mtime assert main.loaded_modules[a_path].x == 1 - + assert logger.messages['info'] == ['loading module: _a', 'reloading module: _a', 'cannot unload module _a'] del_loaded_modules(main) @@ -284,10 +285,11 @@ def test_reload_should_skip_single_good_unchanged_script(tmpdir, empty_config, l main.load_or_reload_modules(main.module_paths_for_user) + ## changing script, but NOT changing mtime a_script.write("""x=1""") # set the mtime to the old mtime, so natlink should NOT reload a_script.setmtime(mtime) - mtime += 1.0 + # mtime += 1.0 main.seen.clear() # is done in trigger_load main.load_or_reload_modules(main.module_paths_for_user) assert set(main.loaded_modules.keys()) == {a_path} @@ -298,9 +300,9 @@ def test_reload_should_skip_single_good_unchanged_script(tmpdir, empty_config, l # make sure it still has the old value, not the new one assert main.loaded_modules[a_path].x == 0 - ## TODO how solve this (QH) - msg = 'skipping unchanged loaded module: _a' - assert msg in logger.messages['debug'] + # removed this message, because of too many messages... + # msg = 'skipping unchanged loaded module: _a' + assert logger.messages['debug'] == [] del_loaded_modules(main) @@ -410,8 +412,10 @@ def test_reload_should_skip_single_bad_unchanged_script(tmpdir, empty_config, lo assert set(main.load_attempt_times.keys()) == {a_path} assert main.load_attempt_times[a_path] == mtime - msg = 'skipping unchanged bad module: _a' - assert msg in logger.messages['info'] + # removed debug message, because of too many debug lines (QH) + # msg = 'skipping unchanged bad module: _a' + # the debug messages are the exception when loading the bad module + assert len(logger.messages['debug']) == 2 del_loaded_modules(main) @@ -478,6 +482,43 @@ def test_load_single_bad_script_that_was_previously_good(tmpdir, empty_config, l del_loaded_modules(main) # +def test_unload_all_loaded_modules(tmpdir, empty_config, logger, monkeypatch): + config = empty_config + config.directories_by_user['user'] = [tmpdir.strpath] + a_script = tmpdir.join('_a.py') + a_path = Path(a_script.strpath) + mtime = 123456.0 + a_script.write("""x=0""") + a_script.setmtime(mtime) + monkeypatch.setattr(time, 'time', lambda: mtime) + + bad_script = tmpdir.join('_bad.py') + bad_path = Path(bad_script.strpath) + mtime = 123456.0 + bad_script.write("""x=; #a syntax error.""") + bad_script.setmtime(mtime) + monkeypatch.setattr(time, 'time', lambda: mtime) + + main = NatlinkMain(logger, config) + main.config = config + main.__init__(logger=logger, config=config) + + _modules = main.module_paths_for_user + assert main.module_paths_for_user == [] + main.user = 'user' # this way, because now user is a property + _modules = main.module_paths_for_user + assert main.module_paths_for_user == [a_path, bad_path] + + main.load_or_reload_modules(main.module_paths_for_user) + _mainkeys = set(main.loaded_modules.keys()) + assert set(main.loaded_modules.keys()) == {a_path} + assert len(main.bad_modules) == 1 + # assume unloading takes place, because module has a unload attribute + main.unload_all_loaded_modules() + assert logger.messages['info'] == ['loading module: _a', 'loading module: _bad', 'cannot unload module _a'] + assert main.bad_modules == set() + + if __name__ == "__main__": diff --git a/tests/test_nsformat.py b/tests/test_nsformat.py new file mode 100644 index 0000000..95fc14d --- /dev/null +++ b/tests/test_nsformat.py @@ -0,0 +1,76 @@ +"""test_nsformat + +Testing nsformat + + Quintijn Hoogenboom, Oct 2023 +""" +#pylint:disable=C0115, C0116 +#pylint:disable=E1101 +import pytest +from natlinkcore import nsformat + + +def test_formatWords(): + Input = "hello there".split() + assert(nsformat.formatWords(Input)) == ("Hello there", set()) + + Input = ["Sentence", "end", r".\period\period", "next", r".\period\period"] + result_text, state = nsformat.formatWords(Input) + assert result_text == "Sentence end. Next." + assert state == {9, 4} + + Next = ["continue"] + result_text, new_state = nsformat.formatWords(Next, state=state) + assert result_text == " Continue" + assert new_state == set() + + sentence = "this is wrong." + with pytest.raises(AssertionError): + nsformat.formatWords(sentence) + + +def test_formatString(): + + sentence = 'hello world. this is a test.' + result, state = nsformat.formatString(sentence) + assert result == "Hello world. This is a test." + assert state == {9, 4} + total = [result] + + sentence = 'continue with? what the fuck!' + result, state = nsformat.formatString(sentence, state=state) + assert result == " Continue with? What the fuck!" + assert state == {9, 4} + total.append(result) + + sentence = 'continue' + result, state = nsformat.formatString(sentence, state=state) + total.append(result) + + sentence = 'normal' + result, state = nsformat.formatString(sentence, state=state) + total.append(result) + total_string = ''.join(total) + + + sentence = ', just normal' + result, state = nsformat.formatString(sentence, state=state) + total.append(result) + total_string = ''.join(total) + + assert total_string == "Hello world. This is a test. Continue with? What the fuck! Continue normal, just normal" + assert state == set() + + # new example: + sentence = 'hello again: this is a test- even if "quoted words" are not dealt with!' + result, state = nsformat.formatString(sentence) + assert result == 'Hello again: this is a test-even if "quoted words" are not dealt with!' + assert state == {9, 4} + total = [result] + + + +# import sys +if __name__ == "__main__": + pytest.main(['test_nsformat.py']) + \ No newline at end of file