From a0ad16f0864b2f29abc5bde76f18e19707c4b3fa Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Fri, 2 Feb 2024 15:27:31 +0100 Subject: [PATCH 01/15] was lost in the flow, reentered from natlinkcore repository --- src/natlinkcore/natlinkpydebug.py | 86 +++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/natlinkcore/natlinkpydebug.py diff --git a/src/natlinkcore/natlinkpydebug.py b/src/natlinkcore/natlinkpydebug.py new file mode 100644 index 0000000..f5ecb24 --- /dev/null +++ b/src/natlinkcore/natlinkpydebug.py @@ -0,0 +1,86 @@ +""" +code to help with debugging including: +- enable python debuggers to attach. +currently on DAP debuggers are supported. +https://microsoft.github.io/debug-adapter-protocol/ +There are several, Microsoft Visual Studio COde is known to work. +There are several, Microsoft Visual Studio COde is known to work. + +If you know how to add support for another debugger please add it. + +Written by Doug Ransom, 2021 +""" +#pylint:disable=C0116, W0703 +import os +import debugpy +from natlinkcore import natlinkstatus + +__status = natlinkstatus.NatlinkStatus() +__natLinkPythonDebugPortEnviornmentVar= "NatlinkPyDebugPort" +__natLinkPythonDebugOnStartupVar="NatlinkPyDebugStartup" + +__pyDefaultPythonExecutor = "python.exe" +__debug_started=False +default_debugpy_port=7474 +__debugpy_debug_port=default_debugpy_port +__debugger="not configured" +dap="DAP" + +#bring a couple functions from DAP and export from our namespace +dap_is_client_connected=debugpy.is_client_connected +dap_breakpoint = debugpy.breakpoint + +def dap_info(): + return f""" +Debugger: {__debugger} DAP Port:{__debugpy_debug_port} IsClientConnected: {dap_is_client_connected()} Default DAP Port {default_debugpy_port} +Debug Started:{__debug_started} +""" + +def start_dap(): + #pylint:disable=W0603 + global __debug_started,__debugpy_debug_port,__debugger + if __debug_started: + print(f"DAP already started with debugpy for port {__debugpy_debug_port}") + return + try: + + if __natLinkPythonDebugPortEnviornmentVar in os.environ: + natLinkPythonPortStringVal = os.environ[__natLinkPythonDebugPortEnviornmentVar] + __debugpy_debug_port = int(natLinkPythonPortStringVal) + print(f"Starting debugpy on port {natLinkPythonPortStringVal}") + + python_exec = __pyDefaultPythonExecutor #for now, only the python in system path can be used for natlink and this module + print(f"Python Executable (required for debugging): '{python_exec}'") + debugpy.configure(python=f"{python_exec}") + debugpy.listen(__debugpy_debug_port) + print(f"debugpy listening on port {__debugpy_debug_port}") + __debug_started = True + __debugger = dap + + if __natLinkPythonDebugOnStartupVar in os.environ: + dos_str=os.environ[__natLinkPythonDebugOnStartupVar] + dos=len(dos_str)==1 and dos_str in "YyTt" + + if dos: + print(f"Waiting for DAP debugger to attach now as {__natLinkPythonDebugOnStartupVar} is set to {dos_str}") + debugpy.wait_for_client() + + + except Exception as ee: + print(f""" + Exception {ee} while starting debug. Possible cause is incorrect python executable specified {python_exec} +""" ) + +def debug_check_on_startup(): + #pylint:disable=W0603 + global __debug_started,__debugpy_debug_port,__debugger + debug_instructions = f"{__status.getCoreDirectory()}\\debugging python instructions.docx" + print(f"Instructions for attaching a python debugger are in {debug_instructions} ") + if __natLinkPythonDebugPortEnviornmentVar in os.environ: + start_dap() + + + + + + From cf994d4c4aef930590a905cfaa262813683cd068 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Fri, 2 Feb 2024 15:28:25 +0100 Subject: [PATCH 02/15] entered getUnimacroGrammarsDirectory, and a lot of tidying up --- src/natlinkcore/natlinkstatus.py | 36 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/natlinkcore/natlinkstatus.py b/src/natlinkcore/natlinkstatus.py index 816fe14..aae0489 100644 --- a/src/natlinkcore/natlinkstatus.py +++ b/src/natlinkcore/natlinkstatus.py @@ -68,6 +68,9 @@ getUnimacroDataDirectory: get the directory where Unimacro grammars can store data, this should be per computer, and is set into the natlink_user area +getUnimacroGrammarsDirectory: get the directory where Unimacro grammars are (by default) located in a sub directory 'UnimacroGrammars' of the + UnimacroDirectory + 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. @@ -154,6 +157,7 @@ def __init__(self): self.UnimacroUserDirectory = None # self.UnimacroGrammarsDirectory = None self.UnimacroDataDirectory = None + self.UnimacroGrammarsDirectory = None ## Vocola: self.VocolaUserDirectory = None self.VocolaDirectory = None @@ -404,7 +408,27 @@ def getUnimacroDirectory(self): return "" self.UnimacroDirectory = unimacro.__path__[-1] return self.UnimacroDirectory + + def getUnimacroGrammarsDirectory(self): + """return the path to the UnimacroGrammarDirectory + + This is the directory UnimacroGrammars below the unimacro directory (most in site-packages) + is normally got via `pip install unimacro` + Can be changed manually in "natlink.ini" as "unimacrogrammarsdirectory = dir-of-your-choice". (section: [unimacro]) + + Note: unimacro grammars can also be put into other "[directories]" in your natlink.ini file. + + """ + if self.UnimacroGrammarsDirectory is not None: + return self.UnimacroGrammarsDirectory + + key = 'unimacrogrammarsdirectory' + value = self.natlinkmain.getconfigsetting(section='directories', option=key) + um_grammars_dir = natlinkcore.config.expand_path(value) + + self.UnimacroGrammarsDirectory = um_grammars_dir + return um_grammars_dir def getUnimacroDataDirectory(self): """return the path to the directory where grammars can store data. @@ -595,14 +619,10 @@ def getVocolaGrammarsDirectory(self): if self.VocolaGrammarsDirectory is not None: return self.VocolaGrammarsDirectory - natlink_user_dir = self.getNatlink_Userdir() - - voc_grammars_dir = Path(natlink_user_dir)/'VocolaGrammars' - if not voc_grammars_dir.is_dir(): - voc_grammars_dir.mkdir() - voc_grammars_dir = str(voc_grammars_dir) + key = 'vocolagrammarsdirectory' + value = self.natlinkmain.getconfigsetting(section='directories', option=key) + voc_grammars_dir = natlinkcore.config.expand_path(value) self.VocolaGrammarsDirectory = voc_grammars_dir - return voc_grammars_dir def getAhkUserDir(self): @@ -807,7 +827,7 @@ def getNatlinkStatusString(self): ## Unimacro: if D['unimacroIsEnabled']: self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is enabled") - for key in ('UnimacroUserDirectory', 'UnimacroDirectory', 'UnimacroDataDirectory'): + for key in ('UnimacroUserDirectory', 'UnimacroDirectory', 'UnimacroDataDirectory', 'UnimacroGrammarsDirectory'): self.appendAndRemove(L, D, key) else: self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is disabled") From eb6845cda5278318b955b12310d282a2681173bb Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Sat, 3 Feb 2024 14:03:39 +0100 Subject: [PATCH 03/15] multiple changes in natlinkstatus.py and natlinkconfigfunctions.py, letting eg unimacro setup be better add getUnimacroGrammarsDirectory --- .../configure/natlinkconfigfunctions.py | 157 +++++------------- src/natlinkcore/natlinkstatus.py | 28 ---- 2 files changed, 41 insertions(+), 144 deletions(-) diff --git a/src/natlinkcore/configure/natlinkconfigfunctions.py b/src/natlinkcore/configure/natlinkconfigfunctions.py index 409c680..aa9c336 100644 --- a/src/natlinkcore/configure/natlinkconfigfunctions.py +++ b/src/natlinkcore/configure/natlinkconfigfunctions.py @@ -8,7 +8,7 @@ # Quintijn Hoogenboom, January 2008 (...), August 2022 # -#pylint:disable=C0302, W0702, R0904, C0116, W0613, R0914, R0912, C0415, W0611 +#pylint:disable=C0302, W0702, R0904, C0116, W0613, R0914, R0912 """With the functions in this module Natlink can be configured. These functions are called in different ways: @@ -57,13 +57,6 @@ def get_check_config_locations(self): """ config_path, fallback_path = loader.config_locations() - if isfile(config_path): - with open(config_path, 'r', encoding='utf-8') as fp: - text = fp.read().strip() - if not text: - print(f'empty natlink.ini file: "{config_path}",\n\tremove, and go back to default') - os.remove(config_path) - if not isfile(config_path): config_dir = Path(config_path).parent if not config_dir.is_dir(): @@ -75,43 +68,7 @@ def check_config(self): """check config_file for possibly unwanted settings """ self.config_remove(section='directories', option='default_config') - keys = self.config_get('directories') - - ## check vocola: - if 'vocoladirectory' in keys and 'vocolagrammarsdirectory' in keys: - try: - import vocola2 - except ImportError: - # vocola has been gone, remove: - self.disable_vocola() - self.config_remove('vocola', 'vocolauserdirectory') - else: - ## just to be sure: - self.config_remove('vocola', 'vocolauserdirectory') - self.config_remove('directories', 'vocoladirectory') - self.config_remove('directories', 'vocolagrammarsdirectory') - if 'unimacrodirectory' in keys and 'unimacrogrammarsdirectory' in keys: - try: - import unimacro - except ImportError: - # unimacro has been gone, remove: - self.disable_unimacro() - self.config_remove('unimacro', 'unimacrouserdirectory') - else: - ## just to be sure: - self.config_remove('unimacro', 'unimacrouserdirectory') - self.config_remove('directories', 'unimacrodirectory') - self.config_remove('directories', 'unimacrogrammarsdirectory') - - - if 'dragonflyuserdirectory' in keys: - try: - import dragonfly - except ImportError: - # dragonfly has been gone, remove: - self.disable_dragonfly() - def getConfig(self): """return the config instance """ @@ -122,16 +79,14 @@ def getConfig(self): self.config_encoding = rwfile.encoding return _config - def config_get(self, section, option=None): - """get the section keys or a setting from the natlink ini file + def config_get(self, section, option): + """set a setting into the natlink ini file """ - if option: - try: - return self.Config.get(section, option) - except (configparser.NoSectionError, configparser.NoOptionError): - return None - return self.Config.options(section) + try: + return self.Config.get(section, option) + except (configparser.NoSectionError, configparser.NoOptionError): + return None def config_set(self, section, option, value): """set a setting into an inifile (possibly other than natlink.ini) @@ -152,7 +107,7 @@ def config_set(self, section, option, value): value = str(value) self.Config.set(section, option, str(value)) self.config_write() - self.status.__init__() + self.status = natlinkstatus.NatlinkStatus() return True def config_write(self): @@ -180,7 +135,7 @@ def config_remove(self, section, option): if section not in ['directories', 'settings', 'userenglish-directories', 'userspanish-directories']: self.Config.remove_section(section) self.config_write() - self.status.__init__() + self.status = natlinkstatus.NatlinkStatus() # def setUserDirectory(self, arg): # self.setDirectory('UserDirectory', arg) @@ -200,7 +155,7 @@ def setDirectory(self, option, dir_path, section=None): print('No valid directory specified') return - dir_path = dir_path.strip().replace('/', '\\') + dir_path = dir_path.strip() directory = createIfNotThere(dir_path, level_up=1) if not (directory and Path(directory).is_dir()): if directory is False: @@ -284,26 +239,32 @@ def clearFile(self, option, section): def setLogging(self, logginglevel): """Sets the natlink logging output - logginglevel (str) -- CRITICAL, FATAL, ERROR, WARNING, INFO, DEBUG - - This one is used in the natlinkconfig_gui + logginglevel (str) -- Critical, Fatal, Error, Warning, Info, Debug """ - key = 'log_level' - section = 'settings' - value = logginglevel.upper() - old_value = self.config_get(section, key) + # Config.py handles log level str upper formatting from ini + value = logginglevel.title() + old_value = self.config_get('settings', "log_level") if old_value == value: print(f'setLogging, setting is already "{old_value}"') return True - if value in ["CRITICAL", "FATAL", "ERROR", "WARNING", "INFO", "DEBUG"]: + if value in ["Critical", "Fatal", "Error", "Warning", "Info", "Debug"]: print(f'setLogging, setting logging to: "{value}"') - self.config_set(section, key, value) + self.config_set('settings', "log_level", value) if old_value is not None: - self.config_set('previous settings', key, old_value) + self.config_set('previous settings', "log_level", old_value) return True - print(f'Invalid value for setLogging: "{value}"') return False + def disableDebugOutput(self): + """disables the Natlink debug output + """ + key = 'log_level' + # section = 'settings' + old_value = self.config_get('previous settings', key) + if old_value: + self.config_set('settings', key, old_value) + self.config_set('settings', key, 'INFO') + def enable_unimacro(self, arg): unimacro_user_dir = self.status.getUnimacroUserDirectory() if unimacro_user_dir and isdir(unimacro_user_dir): @@ -313,7 +274,7 @@ def enable_unimacro(self, arg): uni_dir = self.status.getUnimacroDirectory() if uni_dir: - print('==== install and/or update unimacro====\n') + print('==== instal and/or update unimacro====\n') try: subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "unimacro"]) except subprocess.CalledProcessError: @@ -359,7 +320,7 @@ def enable_vocola(self, arg): voc_dir = self.status.getVocolaDirectory() if voc_dir: - print('==== install and/or update vocola2====\n') + print('==== instal and/or update vocola2====\n') try: subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "vocola2"]) except subprocess.CalledProcessError: @@ -394,41 +355,6 @@ def disable_vocola(self, arg=None): self.config_remove('directories', 'vocola') self.config_remove('directories', 'vocoladirectory') #could still be there... - def enable_dragonfly(self, arg): - """enable dragonfly, by setting arg (prompting if False), and other settings - """ - key = 'dragonflyuserdirectory' - dragonfly_user_dir = self.status.getDragonflyUserDirectory() - if dragonfly_user_dir and isdir(dragonfly_user_dir): - print(f'dragonflyUserDirectory is already defined: "{dragonfly_user_dir}"\n\tto change, first clear (option "D") and then set again') - print('\nWhen you want to upgrade dragonfly (dragonfly2), also first clear ("D"), then choose this option ("d") again.\n') - return - - dfl_prev_dir = self.config_get('previous settings', key) - if dfl_prev_dir: - - print('==== install and/or update dragonfly2====\n') - try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "dragonfly2"]) - except subprocess.CalledProcessError: - print('====\ncould not pip install --upgrade dragonfly2\n====\n') - return - else: - try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "dragonfly2"]) - except subprocess.CalledProcessError: - print('====\ncould not pip install dragonfly2\n====\n') - return - self.status.refresh() # refresh status - - self.setDirectory(key, arg) - - def disable_dragonfly(self, arg=None): - """disable dragonfly, arg not needed/used - """ - key = 'dragonflyuserdirectory' - self.clearDirectory(key) - def copyUnimacroIncludeFile(self): """copy Unimacro include file into Vocola user directory @@ -525,20 +451,20 @@ def includeUnimacroVchLineInVocolaFiles(self, subDirectory=None): changed = 0 correct = 0 Output = [] - rwfile = readwritefile.ReadWriteFile() - lines = rwfile.readAnything(F).split('\n') - for line in lines: + for line in open(F, 'r'): if line.strip() == includeLine.strip(): correct = 1 - if line.strip() in oldIncludeLines: - changed = 1 - continue - Output.append(line) + for oldLine in oldIncludeLines: + if line.strip() == oldLine: + changed = 1 + break + else: + Output.append(line) if changed or not correct: # changes were made: if not correct: Output.insert(0, includeLine) - rwfile.writeAnything(F, Output) + open(F, 'w').write(''.join(Output)) nFiles += 1 elif len(f) == 3 and os.path.isdir(F): # subdirectory, recursive @@ -584,10 +510,7 @@ def removeUnimacroVchLineInVocolaFiles(self, subDirectory=None): if f.endswith(".vcl"): changed = 0 Output = [] - rwfile = readwritefile.ReadWriteFile() - lines = rwfile.readAnything(F).split('\n') - - for line in lines: + for line in open(F, 'r'): for oldLine in oldIncludeLines: if line.strip() == oldLine: changed = 1 @@ -596,10 +519,11 @@ def removeUnimacroVchLineInVocolaFiles(self, subDirectory=None): Output.append(line) if changed: # had break, so changes were made: - rwfile.writeAnything(F, Output) + open(F, 'w').write(''.join(Output)) nFiles += 1 elif len(f) == 3 and os.path.isdir(F): self.removeUnimacroVchLineInVocolaFiles(F) + self.disableVocolaTakesUnimacroActions() mess = f'removed include lines from {nFiles} files in {toFolder}' print(mess) @@ -749,3 +673,4 @@ def createIfNotThere(path_name, level_up=None): _home_path = _nc.home_path _natlinkconfig_path = _nc.natlinkconfig_path print(f'natlinkconfig_path: {_natlinkconfig_path}') + pass diff --git a/src/natlinkcore/natlinkstatus.py b/src/natlinkcore/natlinkstatus.py index aae0489..260243f 100644 --- a/src/natlinkcore/natlinkstatus.py +++ b/src/natlinkcore/natlinkstatus.py @@ -450,34 +450,6 @@ 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 getNatlinkDirectory(self): """return the path of the NatlinkDirectory, where the _natlink_core.pyd package (C++ code) is """ From 74f25546bf9d9b80ffe35d6a90e44ff34326e95f Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Sat, 3 Feb 2024 14:04:35 +0100 Subject: [PATCH 04/15] add 2 tests execscriptest.py is ok, buttonclicktest.py fails. The cause is playEvents. --- tests/buttonclicktest.py | 14 ++++++++++++++ tests/execscriptest.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/buttonclicktest.py create mode 100644 tests/execscriptest.py diff --git a/tests/buttonclicktest.py b/tests/buttonclicktest.py new file mode 100644 index 0000000..e7ff232 --- /dev/null +++ b/tests/buttonclicktest.py @@ -0,0 +1,14 @@ +### buttonClick seems to cause an "ESP" runtime error with Dragon 16. + +import natlink +from dtactions.unimacro import unimacroutils + +if __name__ == "__main__": + try: + natlink.natConnect() + print('try a buttonclick') + natlinkutils.buttonClick('left', 1) + print('after the buttonClick') + finally: + natlink.natDisconnect() + diff --git a/tests/execscriptest.py b/tests/execscriptest.py new file mode 100644 index 0000000..ba555d0 --- /dev/null +++ b/tests/execscriptest.py @@ -0,0 +1,20 @@ +### a buttonClick via natlink.execScript results in a runtimeerror (with Dragon 16) +# +# # File "C:\Program Files (x86)\Natlink\site-packages\natlink\__init__.py", line 82, in execScript +# # return _execScript(script_w,args) +# # natlink.SyntaxError: Error 63334 compiling script execScript (line 1) + + +import natlink +from dtactions.unimacro import unimacroutils + + +if __name__ == "__main__": + try: + natlink.natConnect() + print('try a buttonclick via execScript') + unimacroutils.buttonClick('left', 2) + print('after the buttonClick via execScript') + finally: + natlink.natDisconnect() + From 3cfc5ce223e17724263f4c513754e4ec48a5acfe Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Sun, 4 Feb 2024 16:42:03 +0100 Subject: [PATCH 05/15] the buttonclicktest.py shows the bug when natlink.playEvents is called. --- tests/buttonclicktest.py | 15 ++++++++++++++- tests/execscriptest.py | 20 -------------------- 2 files changed, 14 insertions(+), 21 deletions(-) delete mode 100644 tests/execscriptest.py diff --git a/tests/buttonclicktest.py b/tests/buttonclicktest.py index e7ff232..ded28b0 100644 --- a/tests/buttonclicktest.py +++ b/tests/buttonclicktest.py @@ -1,14 +1,27 @@ ### buttonClick seems to cause an "ESP" runtime error with Dragon 16. +# via unimacroutils it runs via natlink.execScript + +# via natlinkutils, the code runs (the right click is performed), but afterwards +# the "ESP" error is hit. + +# When Dragon is running, it freezes, and must be closed with the windows task manager + import natlink +from natlinkcore import natlinkutils from dtactions.unimacro import unimacroutils if __name__ == "__main__": try: natlink.natConnect() print('try a buttonclick') - natlinkutils.buttonClick('left', 1) + unimacroutils.buttonClick('left', 2) print('after the buttonClick') + + print('now via natlinkutils.buttonClick (right click)') + print('the code runs, but the "ESP" error window appears.') + natlinkutils.buttonClick('right', 1) + print('after the natlinkutils.buttonClick') finally: natlink.natDisconnect() diff --git a/tests/execscriptest.py b/tests/execscriptest.py deleted file mode 100644 index ba555d0..0000000 --- a/tests/execscriptest.py +++ /dev/null @@ -1,20 +0,0 @@ -### a buttonClick via natlink.execScript results in a runtimeerror (with Dragon 16) -# -# # File "C:\Program Files (x86)\Natlink\site-packages\natlink\__init__.py", line 82, in execScript -# # return _execScript(script_w,args) -# # natlink.SyntaxError: Error 63334 compiling script execScript (line 1) - - -import natlink -from dtactions.unimacro import unimacroutils - - -if __name__ == "__main__": - try: - natlink.natConnect() - print('try a buttonclick via execScript') - unimacroutils.buttonClick('left', 2) - print('after the buttonClick via execScript') - finally: - natlink.natDisconnect() - From 2bf1f67e41cea8ff44e6c41c3f2ed25741690907 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Tue, 6 Feb 2024 16:24:06 +0100 Subject: [PATCH 06/15] also enter dragonfly (dragonfly2) in the config procedure, including testing for upgrades.. --- .../configure/natlinkconfig_gui.py | 6 +-- .../configure/natlinkconfigfunctions.py | 40 ++++++++++++++++++- src/natlinkcore/natlinkstatus.py | 26 ++++++++++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/natlinkcore/configure/natlinkconfig_gui.py b/src/natlinkcore/configure/natlinkconfig_gui.py index bddde8a..d7c9422 100644 --- a/src/natlinkcore/configure/natlinkconfig_gui.py +++ b/src/natlinkcore/configure/natlinkconfig_gui.py @@ -1,4 +1,4 @@ -#pylint:disable=W0621, W0703 +#pylint:disable=W0621, W0703, W0603 import sys import platform @@ -116,7 +116,7 @@ def OpenNatlinkConfig(values, event): try: while True: event, values = window.read() - if (event == '-WINDOW CLOSE ATTEMPTED-' or event == 'Exit') and not Thread_Running: + if (event in ['-WINDOW CLOSE ATTEMPTED-', 'Exit']) and not Thread_Running: break # Hidden Columns logic # TODO: if project is enabled, update the project state to enabled. @@ -140,7 +140,7 @@ def OpenNatlinkConfig(values, event): Thread_Running = not Thread_Running elif Thread_Running: - choice = sg.popup(f'Please Wait: Pip install is in progress', keep_on_top=True, custom_text=('Wait','Force Close')) + choice = sg.popup('Please Wait: Pip install is in progress', keep_on_top=True, custom_text=('Wait','Force Close')) if choice == 'Force Close': break diff --git a/src/natlinkcore/configure/natlinkconfigfunctions.py b/src/natlinkcore/configure/natlinkconfigfunctions.py index aa9c336..f63c582 100644 --- a/src/natlinkcore/configure/natlinkconfigfunctions.py +++ b/src/natlinkcore/configure/natlinkconfigfunctions.py @@ -8,7 +8,7 @@ # Quintijn Hoogenboom, January 2008 (...), August 2022 # -#pylint:disable=C0302, W0702, R0904, C0116, W0613, R0914, R0912 +#pylint:disable=C0302, W0702, R0904, C0116, W0613, R0914, R0912, R1732, W1514, W0107 """With the functions in this module Natlink can be configured. These functions are called in different ways: @@ -308,6 +308,44 @@ def disable_unimacro(self, arg=None): self.config_remove('directories', 'unimacrodirectory') # could still be there... self.status.refresh() + ### Dragonfly: + + def enable_dragonfly(self, arg): + dragonfly_user_dir = self.status.getDragonflyUserDirectory() + if dragonfly_user_dir and isdir(dragonfly_user_dir): + print(f'DragonflyUserDirectory is already defined: "{dragonfly_user_dir}"\n\tto change, first clear (option "D") and then set again') + print('\nWhen you want to upgrade Dragonfly, also first clear ("D"), then choose this option ("d") again.\n') + return + + df_dir = self.status.getDragonflyDirectory() + if df_dir: + print('==== instal and/or update dragonfly2====\n') + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "dragonfly2"]) + except subprocess.CalledProcessError: + print('====\ncould not pip install --upgrade dragonfly2\n====\n') + return + else: + try: + subprocess.check_call([sys.executable, "-m", "pip", "install", "dragonfly2"]) + except subprocess.CalledProcessError: + print('====\ncould not pip install dragonfly2\n====\n') + return + self.status.refresh() # refresh status + df_dir = self.status.getDragonflyDirectory() + + self.setDirectory('DragonflyUserDirectory', arg) + dragonfly_user_dir = self.config_get('dragonfly', 'dragonflyuserdirectory') + if not dragonfly_user_dir: + return + + + def disable_dragonfly(self, arg=None): + """disable dragonfly, do not expect arg + """ + self.config_remove('directories', 'dragonflyuserdirectory') # could still be there... + self.status.refresh() + def enable_vocola(self, arg): """enable vocola, by setting arg (prompting if False), and other settings diff --git a/src/natlinkcore/natlinkstatus.py b/src/natlinkcore/natlinkstatus.py index 260243f..3ba31d8 100644 --- a/src/natlinkcore/natlinkstatus.py +++ b/src/natlinkcore/natlinkstatus.py @@ -138,7 +138,7 @@ class NatlinkStatus(metaclass=singleton.Singleton): """ known_directory_options = ['userdirectory', 'dragonflyuserdirectory', - 'unimacrodirectory', + 'unimacrodirectory', 'unimacrogrammarsdirectory', 'vocoladirectory', 'vocolagrammarsdirectory'] def __init__(self): @@ -165,6 +165,9 @@ def __init__(self): ## Dragonfly self.DragonflyDirectory = None self.DragonflyUserDirectory = None + ## dtactions + self.DtactionsDirectory = None + ## AutoHotkey: self.AhkUserDir = None self.AhkExeDir = None @@ -500,12 +503,12 @@ def getDragonflyDirectory(self): if self.DragonflyDirectory is not None: return self.DragonflyDirectory try: - import dragonfly + import dragonfly2 except ImportError: self.DragonflyDirectory = "" return "" - self.DragonflyDirectory = str(Path(dragonfly.__file__).parent) + self.DragonflyDirectory = str(Path(dragonfly2.__file__).parent) return self.DragonflyDirectory @@ -597,6 +600,20 @@ def getVocolaGrammarsDirectory(self): self.VocolaGrammarsDirectory = voc_grammars_dir return voc_grammars_dir + def getDtactionsDirectory(self): + """dtactions directory should be found with an import (like getUnimacroDirectory) + """ + + if self.DtactionsDirectory is not None: + return self.DtactionsDirectory + try: + import dtactions + except ImportError: + self.DtactionsDirectory = "" + return "" + self.DtactionsDirectory = dtactions.__path__[-1] + return self.DtactionsDirectory + def getAhkUserDir(self): return self.getAhkUserDirFromIni() @@ -733,6 +750,7 @@ def getNatlinkStatusDict(self): 'UserDirectory', 'DragonflyDirectory', 'DragonflyUserDirectory', 'ExtraGrammarDirectories', + 'DtactionsDirectory', 'InstallVersion', # 'IncludeUnimacroInPythonPath', 'AhkExeDir', 'AhkUserDir']: @@ -803,7 +821,7 @@ def getNatlinkStatusString(self): self.appendAndRemove(L, D, key) else: self.appendAndRemove(L, D, 'unimacroIsEnabled', "---Unimacro is disabled") - for key in ('UnimacroUserDirectory', 'UnimacroDirectory'): + for key in ('UnimacroUserDirectory', 'UnimacroGrammarsDirectory', 'UnimacroDirectory'): del D[key] ## UserDirectory: if D['userIsEnabled']: From 9b2166750efa02dfe89be94079fcb2fc6bc0e46e Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Thu, 8 Feb 2024 11:36:31 +0100 Subject: [PATCH 07/15] add a few changes to nsformat.py (a\\determiner and I\\pronoun a word with \\ (properties) can also have length 2 when splitted into wordList. --- src/natlinkcore/nsformat.py | 6 +++++- tests/test_nsformat.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/natlinkcore/nsformat.py b/src/natlinkcore/nsformat.py index f7bbab2..6a5f478 100644 --- a/src/natlinkcore/nsformat.py +++ b/src/natlinkcore/nsformat.py @@ -104,6 +104,7 @@ propDict['letter'] = (flag_no_space_next,) # lowercase is hardcoded in below. propDict['spelling-letter'] = (flag_no_space_next,) # lowercase is hardcoded in below. propDict['uppercase-letter'] = (flag_no_space_next,) +propDict['determiner'] = tuple() # nothing special #--------------------------------------------------------------------------- # This is the main formatting entry point. It takes the old format state and @@ -419,7 +420,7 @@ def getWordInfo(word): if word.find('\\') == -1: return set() # no flags wList = word.split('\\') - if len(wList) == 3: + if len(wList) in (2,3): prop = wList[1] if not prop: return set() @@ -429,6 +430,9 @@ def getWordInfo(word): return set(propDict['left-double-quote']) if prop.startswith('right-'): return set(propDict['right-double-quote']) + if prop in ['determiner', 'pronoun']: + return set() + print(f'getWordInfo, unknown word property: {prop} {"word"}') return set() # empty tuple # should not come here diff --git a/tests/test_nsformat.py b/tests/test_nsformat.py index 95fc14d..0ec53f7 100644 --- a/tests/test_nsformat.py +++ b/tests/test_nsformat.py @@ -11,6 +11,33 @@ def test_formatWords(): + + ## a\\determiner (Dragon 16??) (added propDict[determiner] = tuple(), and likewise for I\\pronoun) + + Input = ["First", ".\\period\\full stop", "a", "test"] + result = nsformat.formatWords(Input) + print(f'result nsformat: "{result}"') + assert(nsformat.formatWords(Input)) == ("First. A test", set()) + + Input = ["Second", ".\\dot\\dot stop", "a", "test"] + result = nsformat.formatWords(Input) + print(f'result nsformat: "{result}"') + assert(nsformat.formatWords(Input)) == ("Second.a test", set()) + + + + Input = "Third a\\determiner test".split() + result = nsformat.formatWords(Input) + print(f'result nsformat: "{result}"') + assert(nsformat.formatWords(Input)) == ("Third a test", set()) + + Input = "Fourth I\\pronoun test".split() + result = nsformat.formatWords(Input) + print(f'result nsformat: "{result}"') + assert(nsformat.formatWords(Input)) == ("Fourth I test", set()) + + + Input = "hello there".split() assert(nsformat.formatWords(Input)) == ("Hello there", set()) @@ -24,6 +51,10 @@ def test_formatWords(): assert result_text == " Continue" assert new_state == set() + + + + sentence = "this is wrong." with pytest.raises(AssertionError): nsformat.formatWords(sentence) From 10de32895976f86b762a5673328008007f88e715 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Thu, 8 Feb 2024 15:52:58 +0100 Subject: [PATCH 08/15] small change in nsformat.py (formatPassword) --- src/natlinkcore/nsformat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/natlinkcore/nsformat.py b/src/natlinkcore/nsformat.py index 6a5f478..8eea88c 100644 --- a/src/natlinkcore/nsformat.py +++ b/src/natlinkcore/nsformat.py @@ -223,6 +223,7 @@ def formatPassword(wordList): nextRepeat = countDict[w] outList.append(str(nextRepeat)) else: + w = w.split('\\')[0] outList.append(w.capitalize()) return ''.join(outList) From 4cf780630b65949701d383fd14540248d8b4e04c Mon Sep 17 00:00:00 2001 From: Doug Ransom Date: Sat, 17 Feb 2024 06:11:00 -0800 Subject: [PATCH 09/15] use pythong logging module to report output of pip etc. --- .../configure/natlinkconfig_gui.py | 16 +++++ .../configure/natlinkconfigfunctions.py | 28 +++++++-- tests/test_natlinktimer.py | 58 +++++++++---------- 3 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/natlinkcore/configure/natlinkconfig_gui.py b/src/natlinkcore/configure/natlinkconfig_gui.py index d7c9422..580b93c 100644 --- a/src/natlinkcore/configure/natlinkconfig_gui.py +++ b/src/natlinkcore/configure/natlinkconfig_gui.py @@ -3,6 +3,10 @@ import platform import PySimpleGUI as sg +import logging + + + # https://www.pysimplegui.org/en/latest/ from natlinkcore.configure.natlinkconfigfunctions import NatlinkConfig from natlinkcore import natlinkstatus @@ -47,6 +51,8 @@ def collapse(layout, key, visible): [sg.T('Natlink GUI Output')], [sg.Output(size=(40,10), echo_stdout_stderr=True, expand_x=True, key='-OUTPUT-')]] + + #### Main UI Layout #### layout = [[sg.T('Environment:', font='bold'), sg.T(f'Windows OS: {osVersion.major}, Build: {osVersion.build}'), sg.T(f'Python: {pyVersion}'), sg.T(f'Dragon Version: {Status.getDNSVersion()}')], #### Projects Checkbox #### @@ -61,6 +67,9 @@ def collapse(layout, key, visible): window = sg.Window('Natlink configuration GUI', layout, enable_close_attempted_event=True) + + + def ThreadIsRunning(): global Thread_Running Thread_Running = not Thread_Running @@ -114,6 +123,13 @@ def OpenNatlinkConfig(values, event): #### Event Loop #### try: + #we want to set the logger up just before we call window.read() + handler = logging.StreamHandler(sys.stdout) + root = logging.getLogger() + + handler.setLevel(logging.DEBUG) + root.addHandler(handler) + root.setLevel(logging.DEBUG) while True: event, values = window.read() if (event in ['-WINDOW CLOSE ATTEMPTED-', 'Exit']) and not Thread_Running: diff --git a/src/natlinkcore/configure/natlinkconfigfunctions.py b/src/natlinkcore/configure/natlinkconfigfunctions.py index f63c582..2135d04 100644 --- a/src/natlinkcore/configure/natlinkconfigfunctions.py +++ b/src/natlinkcore/configure/natlinkconfigfunctions.py @@ -30,8 +30,24 @@ from natlinkcore import readwritefile from natlinkcore import tkinter_dialogs +import logging + isfile, isdir, join = os.path.isfile, os.path.isdir, os.path.join + +def do_pip(*args): + """ + Run a pip command with args. + Diagnostic logging. + """ + + + command = [sys.executable,"-m", "pip"] + list(args) + logging.info(f"command: {command} ") + completed_process=subprocess.run(command,capture_output=True) + logging.debug(f"completed_process: {completed_process}") + completed_process.check_returncode + class NatlinkConfig: """performs the configuration tasks of Natlink @@ -276,13 +292,13 @@ def enable_unimacro(self, arg): if uni_dir: print('==== instal and/or update unimacro====\n') try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "unimacro"]) + do_pip("install", "--upgrade", "unimacro") except subprocess.CalledProcessError: print('====\ncould not pip install --upgrade unimacro\n====\n') return else: try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "unimacro"]) + do_pip("install", "unimacro") except subprocess.CalledProcessError: print('====\ncould not pip install unimacro\n====\n') return @@ -321,13 +337,13 @@ def enable_dragonfly(self, arg): if df_dir: print('==== instal and/or update dragonfly2====\n') try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "dragonfly2"]) + do_pip( "install", "--upgrade", "dragonfly2") except subprocess.CalledProcessError: print('====\ncould not pip install --upgrade dragonfly2\n====\n') return else: try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "dragonfly2"]) + do_pip( "install", "dragonfly2") except subprocess.CalledProcessError: print('====\ncould not pip install dragonfly2\n====\n') return @@ -360,13 +376,13 @@ def enable_vocola(self, arg): if voc_dir: print('==== instal and/or update vocola2====\n') try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "vocola2"]) + do_pip("install", "--upgrade", "vocola2") except subprocess.CalledProcessError: print('====\ncould not pip install --upgrade vocola2\n====\n') return else: try: - subprocess.check_call([sys.executable, "-m", "pip", "install", "vocola2"]) + do_pip("install", "vocola2") except subprocess.CalledProcessError: print('====\ncould not pip install vocola2\n====\n') return diff --git a/tests/test_natlinktimer.py b/tests/test_natlinktimer.py index 57f4307..f5ea785 100644 --- a/tests/test_natlinktimer.py +++ b/tests/test_natlinktimer.py @@ -144,36 +144,36 @@ def toggleMicrophone(self): def testStopAtMicOff(): - try: - natlink.natConnect() - testGram = TestGrammar(name="stop_at_mic_off") - testGram.resetExperiment() - testGram.interval = 100 # all milliseconds - testGram.sleepTime = 20 - testGram.MaxHit = 6 - testGram.toggleMicAt = 3 - assert natlinktimer.getNatlinktimerStatus() in (0, None) - natlinktimer.setTimerCallback(testGram.doTimerClassic, interval=testGram.interval, callAtMicOff=testGram.cancelMode, debug=debug) - ## 1 timer active: - for _ in range(5): - if testGram.toggleMicAt and testGram.Hit >= testGram.toggleMicAt: - break - if testGram.Hit >= testGram.MaxHit: - break - wait(500) # 0.5 second - if debug: - print(f'waited 0.1 second for timer to finish testGram, Hit: {testGram.Hit} ({testGram.MaxHit})') - else: - raise TestError(f'not enough time to finish the testing procedure (came to {testGram.Hit} of {testGram.MaxHit})') - print(f'testGram.results: {testGram.results}') - assert len(testGram.results) == testGram.toggleMicAt - assert natlinktimer.getNatlinktimerStatus() == 0 ## natlinktimer is NOT destroyed after last timer is gone. - assert testGram.startCancelMode is True - assert testGram.endCancelMode is True + + with natlink.natConnect(): + try: + testGram = TestGrammar(name="stop_at_mic_off") + testGram.resetExperiment() + testGram.interval = 100 # all milliseconds + testGram.sleepTime = 20 + testGram.MaxHit = 6 + testGram.toggleMicAt = 3 + assert natlinktimer.getNatlinktimerStatus() in (0, None) + natlinktimer.setTimerCallback(testGram.doTimerClassic, interval=testGram.interval, callAtMicOff=testGram.cancelMode, debug=debug) + ## 1 timer active: + for _ in range(5): + if testGram.toggleMicAt and testGram.Hit >= testGram.toggleMicAt: + break + if testGram.Hit >= testGram.MaxHit: + break + wait(500) # 0.5 second + if debug: + print(f'waited 0.1 second for timer to finish testGram, Hit: {testGram.Hit} ({testGram.MaxHit})') + else: + raise TestError(f'not enough time to finish the testing procedure (came to {testGram.Hit} of {testGram.MaxHit})') + print(f'testGram.results: {testGram.results}') + assert len(testGram.results) == testGram.toggleMicAt + assert natlinktimer.getNatlinktimerStatus() == 0 ## natlinktimer is NOT destroyed after last timer is gone. + assert testGram.startCancelMode is True + assert testGram.endCancelMode is True - finally: - natlinktimer.stopTimerCallback() - natlink.natDisconnect() + finally: + natlinktimer.stopTimerCallback() # def testStopAtMicOff(): From 15f8045b4df8fa7a0afdaef72d8798f7b32892c9 Mon Sep 17 00:00:00 2001 From: Doug Ransom Date: Sat, 17 Feb 2024 08:18:42 -0800 Subject: [PATCH 10/15] command line uses logging. --- pyproject.toml | 1 + src/natlinkcore/configure/foo.txt | 5 + src/natlinkcore/configure/loggert.py | 14 +++ .../configure/natlinkconfig_cli.py | 18 +++- .../configure/natlinkconfig_gui.py | 53 +++++++++- .../configure/natlinkconfigfunctions.py | 96 +++++++++---------- 6 files changed, 132 insertions(+), 55 deletions(-) create mode 100644 src/natlinkcore/configure/foo.txt create mode 100644 src/natlinkcore/configure/loggert.py diff --git a/pyproject.toml b/pyproject.toml index f79257f..0a7c749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies= [ "pysimplegui>=4.60.3", "pydebugstring", "dtactions>=1.5.7", + "platformdirs >= 4.2.0" ] classifiers=[ "Development Status :: 4 - Beta", diff --git a/src/natlinkcore/configure/foo.txt b/src/natlinkcore/configure/foo.txt new file mode 100644 index 0000000..7c027fe --- /dev/null +++ b/src/natlinkcore/configure/foo.txt @@ -0,0 +1,5 @@ +ERROR: Could not find a version that satisfies the requirement foobar32 (from versions: none) +ERROR: No matching distribution found for foobar32 + +[notice] A new release of pip available: 22.3.1 -> 24.0 +[notice] To update, run: python.exe -m pip install --upgrade pip diff --git a/src/natlinkcore/configure/loggert.py b/src/natlinkcore/configure/loggert.py new file mode 100644 index 0000000..9580857 --- /dev/null +++ b/src/natlinkcore/configure/loggert.py @@ -0,0 +1,14 @@ +import logging +import sys +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +#formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +#handler.setFormatter(formatter) + +root = logging.getLogger() +root.addHandler(handler) +root.setLevel(logging.DEBUG) +logging.debug("A") +logging.info("B") +logging.log(logging.DEBUG,"C") +print("\nD E") diff --git a/src/natlinkcore/configure/natlinkconfig_cli.py b/src/natlinkcore/configure/natlinkconfig_cli.py index 7bb074e..466b2a3 100644 --- a/src/natlinkcore/configure/natlinkconfig_cli.py +++ b/src/natlinkcore/configure/natlinkconfig_cli.py @@ -5,9 +5,10 @@ import os import os.path from natlinkcore.configure import extensions_and_folders - +from platformdirs import user_log_dir +from pathlib import Path from natlinkcore.configure import natlinkconfigfunctions - +import logging def _main(Options=None): """Catch the options and perform the resulting command line functions @@ -17,6 +18,19 @@ def _main(Options=None): etc., usage above... """ + appname="natlinkconfig_cli" + logdir = Path(user_log_dir(appname=appname,ensure_exists=True)) + logfilename=logdir/f"{appname}.txt" + file_handler = logging.FileHandler(logfilename) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + logfile_logger = logging.getLogger() + + #always leave at debug. So we have this if there is ever a problem. + file_handler.setLevel(logging.DEBUG) + logfile_logger.addHandler(file_handler) + logfile_logger.setLevel(logging.DEBUG) + cli = CLI() cli.Config = natlinkconfigfunctions.NatlinkConfig() shortOptions = "DVNOHKaAiIxXbBuqe" diff --git a/src/natlinkcore/configure/natlinkconfig_gui.py b/src/natlinkcore/configure/natlinkconfig_gui.py index 580b93c..8491736 100644 --- a/src/natlinkcore/configure/natlinkconfig_gui.py +++ b/src/natlinkcore/configure/natlinkconfig_gui.py @@ -4,6 +4,20 @@ import PySimpleGUI as sg import logging +from platformdirs import user_log_dir +from pathlib import Path +appname="natlinkconfig_gui" +logdir = Path(user_log_dir(appname=appname,ensure_exists=True)) +logfilename=logdir/"config_gui_log.txt" +file_handler = logging.FileHandler(logfilename) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler.setFormatter(formatter) +logfile_logger = logging.getLogger() + +#always leave at debug. So we have this if there is ever a problem. +file_handler.setLevel(logging.DEBUG) +logfile_logger.addHandler(file_handler) +logfile_logger.setLevel(logging.DEBUG) @@ -18,6 +32,8 @@ Config = NatlinkConfig() Status = natlinkstatus.NatlinkStatus() + + SYMBOL_UP = '▲' SYMBOL_DOWN = '▼' @@ -67,7 +83,21 @@ def collapse(layout, key, visible): window = sg.Window('Natlink configuration GUI', layout, enable_close_attempted_event=True) - + #this is for the GUI logging. +#set the level on this corresponding to the natlink levels later. +#This must be created after the window is created, or nothing gets logged. + +#and we don't add the handler until just before the window is read the first time +#because a write during this period will cause a problem. + +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +try: + ll=Config.config_get("settings","log_level") + if ll != "Debug": + handler.setLevel(logging.INFO) +except Exception as ee: + logging.debug(f"{__file__} failed to get log_level {ee} ") def ThreadIsRunning(): @@ -78,6 +108,13 @@ def ThreadIsRunning(): # Natlink def SetNatlinkLoggingOutput(values, event): Config.setLogging(values['Set_Logging_Natlink']) + #also if natlink is not debug level, use info for logging in this application also. + try: + ll=Config.config_get("settings","log_level") + if ll != "Debug": + handler.setLevel(logging.INFO) + except Exception as ee: + logging.debug(f"{__file__} failed to get log_level {ee} ") # Dragonfly2 def Dragonfly2UserDir(values, event): @@ -121,15 +158,21 @@ def OpenNatlinkConfig(values, event): unimacro_dispatch = {'Set_UserDir_Unimacro': UnimacroUserDir, 'Clear_UserDir_Unimacro': UnimacroUserDir} autohotkey_dispatch = {'Set_Exe_Ahk': AhkExeDir, 'Clear_Exe_Ahk': AhkExeDir, 'Set_ScriptsDir_Ahk': AhkUserDir,'Clear_ScriptsDir_Ahk': AhkUserDir} + #### Event Loop #### try: #we want to set the logger up just before we call window.read() - handler = logging.StreamHandler(sys.stdout) - root = logging.getLogger() + #because if something is logged before the first window.read() there will be a failure. + + + + #this would be a good spot to change the logging level, based on the natlink logging level + #note the natlink logging level doesn't correspond one to one with the logging module levels. + logfile_logger.addHandler(handler) handler.setLevel(logging.DEBUG) - root.addHandler(handler) - root.setLevel(logging.DEBUG) + #do not change the logger level from debug + #change it in the handler. Because a file handler logs everything. while True: event, values = window.read() if (event in ['-WINDOW CLOSE ATTEMPTED-', 'Exit']) and not Thread_Running: diff --git a/src/natlinkcore/configure/natlinkconfigfunctions.py b/src/natlinkcore/configure/natlinkconfigfunctions.py index 2135d04..714453c 100644 --- a/src/natlinkcore/configure/natlinkconfigfunctions.py +++ b/src/natlinkcore/configure/natlinkconfigfunctions.py @@ -20,7 +20,7 @@ import shutil import sys import subprocess -from pprint import pprint +from pprint import pformat from pathlib import Path import configparser @@ -46,7 +46,7 @@ def do_pip(*args): logging.info(f"command: {command} ") completed_process=subprocess.run(command,capture_output=True) logging.debug(f"completed_process: {completed_process}") - completed_process.check_returncode + completed_process.check_returncode() class NatlinkConfig: """performs the configuration tasks of Natlink @@ -164,7 +164,7 @@ def setDirectory(self, option, dir_path, section=None): """ section = section or 'directories' if not dir_path: - print('==== Please specify the wanted directory in Dialog window ====\n') + logging.info('==== Please specify the wanted directory in Dialog window ====\n') prev_path = self.config_get('previous settings', option) or self.documents_path dir_path = tkinter_dialogs.GetDirFromDialog(title=f'Please choose a "{option}"', initialdir=prev_path) if not dir_path: @@ -177,9 +177,9 @@ def setDirectory(self, option, dir_path, section=None): if directory is False: directory = config.expand_path(dir_path) if dir_path == directory: - print(f'Cannot set "{option}", the given path is invalid: "{directory}"') + logging.info(f'Cannot set "{option}", the given path is invalid: "{directory}"') else: - print(f'Cannot set "{option}", the given path is invalid: "{directory}" ("{dir_path}")') + logging.info(f'Cannot set "{option}", the given path is invalid: "{directory}" ("{dir_path}")') return nice_dir_path = self.prefix_home(dir_path) @@ -187,9 +187,9 @@ def setDirectory(self, option, dir_path, section=None): self.config_set(section, option, nice_dir_path) self.config_remove('previous settings', option) if section == 'directories': - print(f'Set option "{option}" to "{dir_path}"') + logging.info(f'Set option "{option}" to "{dir_path}"') else: - print(f'Set in section "{section}", option "{option}" to "{dir_path}"') + logging.info(f'Set in section "{section}", option "{option}" to "{dir_path}"') return def clearDirectory(self, option, section=None): @@ -198,7 +198,7 @@ def clearDirectory(self, option, section=None): section = section or 'directories' old_value = self.config_get(section, option) if not old_value: - print(f'The "{option}" was not set, nothing changed...') + logging.info(f'The "{option}" was not set, nothing changed...') return if isValidDir(old_value): self.config_set('previous settings', option, old_value) @@ -206,7 +206,7 @@ def clearDirectory(self, option, section=None): self.config_remove('previous settings', option) self.config_remove(section, option) - print(f'cleared "{option}"') + logging.info(f'cleared "{option}"') def prefix_home(self, dir_path): @@ -226,15 +226,15 @@ def setFile(self, option, file_path, section): prev_path = self.config_get('previous settings', option) or "" file_path = tkinter_dialogs.GetFileFromDialog(title=f'Please choose a "{option}"', initialdir=prev_path) if not file_path: - print('No valid file specified') + logging.info('No valid file specified') return file_path = file_path.strip() if not Path(file_path).is_file(): - print(f'No valid file specified ("{file_path}")') + logging.info(f'No valid file specified ("{file_path}")') self.config_set(section, option, file_path) self.config_remove('previous settings', option) - print(f'Set in section "{section}", option "{option}" to "{file_path}"') + logging.info(f'Set in section "{section}", option "{option}" to "{file_path}"') return def clearFile(self, option, section): @@ -242,7 +242,7 @@ def clearFile(self, option, section): """ old_value = self.config_get(section, option) if not old_value: - print(f'The "{option}" was not set, nothing changed...') + logging.info(f'The "{option}" was not set, nothing changed...') return if isValidFile(old_value): self.config_set('previous settings', option, old_value) @@ -250,7 +250,7 @@ def clearFile(self, option, section): self.config_remove('previous settings', option) self.config_remove(section, option) - print(f'cleared "{option}"') + logging.info(f'cleared "{option}"') def setLogging(self, logginglevel): @@ -261,10 +261,10 @@ def setLogging(self, logginglevel): value = logginglevel.title() old_value = self.config_get('settings', "log_level") if old_value == value: - print(f'setLogging, setting is already "{old_value}"') + logging.info(f'setLogging, setting is already "{old_value}"') return True if value in ["Critical", "Fatal", "Error", "Warning", "Info", "Debug"]: - print(f'setLogging, setting logging to: "{value}"') + logging.info(f'setLogging, setting logging to: "{value}"') self.config_set('settings', "log_level", value) if old_value is not None: self.config_set('previous settings', "log_level", old_value) @@ -284,23 +284,23 @@ def disableDebugOutput(self): def enable_unimacro(self, arg): unimacro_user_dir = self.status.getUnimacroUserDirectory() if unimacro_user_dir and isdir(unimacro_user_dir): - print(f'UnimacroUserDirectory is already defined: "{unimacro_user_dir}"\n\tto change, first clear (option "O") and then set again') - print('\nWhen you want to upgrade Unimacro, also first clear ("O"), then choose this option ("o") again.\n') + logging.info(f'UnimacroUserDirectory is already defined: "{unimacro_user_dir}"\n\tto change, first clear (option "O") and then set again') + logging.info('\nWhen you want to upgrade Unimacro, also first clear ("O"), then choose this option ("o") again.\n') return uni_dir = self.status.getUnimacroDirectory() if uni_dir: - print('==== instal and/or update unimacro====\n') + logging.info('==== instal and/or update unimacro====\n') try: do_pip("install", "--upgrade", "unimacro") except subprocess.CalledProcessError: - print('====\ncould not pip install --upgrade unimacro\n====\n') + logging.info('====\ncould not pip install --upgrade unimacro\n====\n') return else: try: do_pip("install", "unimacro") except subprocess.CalledProcessError: - print('====\ncould not pip install unimacro\n====\n') + logging.info('====\ncould not pip install unimacro\n====\n') return self.status.refresh() # refresh status uni_dir = self.status.getUnimacroDirectory() @@ -329,23 +329,23 @@ def disable_unimacro(self, arg=None): def enable_dragonfly(self, arg): dragonfly_user_dir = self.status.getDragonflyUserDirectory() if dragonfly_user_dir and isdir(dragonfly_user_dir): - print(f'DragonflyUserDirectory is already defined: "{dragonfly_user_dir}"\n\tto change, first clear (option "D") and then set again') - print('\nWhen you want to upgrade Dragonfly, also first clear ("D"), then choose this option ("d") again.\n') + logging.info(f'DragonflyUserDirectory is already defined: "{dragonfly_user_dir}"\n\tto change, first clear (option "D") and then set again') + logging.info('\nWhen you want to upgrade Dragonfly, also first clear ("D"), then choose this option ("d") again.\n') return df_dir = self.status.getDragonflyDirectory() if df_dir: - print('==== instal and/or update dragonfly2====\n') + logging.info('==== instal and/or update dragonfly2====\n') try: do_pip( "install", "--upgrade", "dragonfly2") except subprocess.CalledProcessError: - print('====\ncould not pip install --upgrade dragonfly2\n====\n') + logging.info('====\ncould not pip install --upgrade dragonfly2\n====\n') return else: try: do_pip( "install", "dragonfly2") except subprocess.CalledProcessError: - print('====\ncould not pip install dragonfly2\n====\n') + logging.info('====\ncould not pip install dragonfly2\n====\n') return self.status.refresh() # refresh status df_dir = self.status.getDragonflyDirectory() @@ -368,23 +368,23 @@ def enable_vocola(self, arg): """ vocola_user_dir = self.status.getVocolaUserDirectory() if vocola_user_dir and isdir(vocola_user_dir): - print(f'VocolaUserDirectory is already defined: "{vocola_user_dir}"\n\tto change, first clear (option "V") and then set again') - print('\nWhen you want to upgrade Vocola (vocola2), also first clear ("V"), then choose this option ("v") again.\n') + logging.info(f'VocolaUserDirectory is already defined: "{vocola_user_dir}"\n\tto change, first clear (option "V") and then set again') + logging.info('\nWhen you want to upgrade Vocola (vocola2), also first clear ("V"), then choose this option ("v") again.\n') return voc_dir = self.status.getVocolaDirectory() if voc_dir: - print('==== instal and/or update vocola2====\n') + logging.info('==== instal and/or update vocola2====\n') try: do_pip("install", "--upgrade", "vocola2") except subprocess.CalledProcessError: - print('====\ncould not pip install --upgrade vocola2\n====\n') + logging.info('====\ncould not pip install --upgrade vocola2\n====\n') return else: try: do_pip("install", "vocola2") except subprocess.CalledProcessError: - print('====\ncould not pip install vocola2\n====\n') + logging.info('====\ncould not pip install vocola2\n====\n') return self.status.refresh() # refresh status voc_dir = self.status.getVocolaDirectory() @@ -420,32 +420,32 @@ def copyUnimacroIncludeFile(self): toFolder = Path(self.status.getVocolaUserDirectory()) if not unimacroDir.is_dir(): mess = f'copyUnimacroIncludeFile: unimacroDir "{str(unimacroDir)}" is not a directory' - print(mess) + logging.warn(mess) return fromFile = fromFolder/uscFile if not fromFile.is_file(): mess = f'copyUnimacroIncludeFile: file "{str(fromFile)}" does not exist (is not a valid file)' - print(mess) + logging.warn(mess) return if not toFolder.is_dir(): mess = f'copyUnimacroIncludeFile: vocolaUserDirectory does not exist "{str(toFolder)}" (is not a directory)' - print(mess) + logging.warn(mess) return toFile = toFolder/uscFile if toFolder.is_file(): - print(f'remove previous "{str(toFile)}"') + logging.info(f'remove previous "{str(toFile)}"') try: os.remove(toFile) except: mess = f'copyUnimacroIncludeFile: Could not remove previous version of "{str(toFile)}"' - print(mess) + logging.info(mess) try: shutil.copyfile(fromFile, toFile) - print(f'copied "{uscFile}" from "{str(fromFolder)}" to "{str(toFolder)}"') + logging.info(f'copied "{uscFile}" from "{str(fromFolder)}" to "{str(toFolder)}"') except: mess = f'Could not copy new version of "{uscFile}", from "{str(fromFolder)}" to "{str(toFolder)}"' - print(mess) + logging.warn(mess) return return @@ -458,17 +458,17 @@ def removeUnimacroIncludeFile(self): toFolder = Path(self.status.getVocolaUserDirectory()) if not toFolder.is_dir(): mess = f'removeUnimacroIncludeFile: vocolaUserDirectory does not exist "{str(toFolder)}" (is not a directory)' - print(mess) + logging.warn(mess) return toFile = toFolder/uscFile if toFolder.is_file(): - print(f'remove Unimacro include file "{str(toFile)}"') + logging.info(f'remove Unimacro include file "{str(toFile)}"') try: os.remove(toFile) except: mess = f'copyUnimacroIncludeFile: Could not remove previous version of "{str(toFile)}"' - print(mess) + logging.warning(mess) def includeUnimacroVchLineInVocolaFiles(self, subDirectory=None): """include the Unimacro wrapper support line into all Vocola command files @@ -496,7 +496,7 @@ def includeUnimacroVchLineInVocolaFiles(self, subDirectory=None): if not os.path.isdir(toFolder): mess = f'cannot find Vocola command files directory, not a valid path: {toFolder}' - print(mess) + logging.warning(mess) return mess nFiles = 0 for f in os.listdir(toFolder): @@ -524,7 +524,7 @@ def includeUnimacroVchLineInVocolaFiles(self, subDirectory=None): # subdirectory, recursive self.includeUnimacroVchLineInVocolaFiles(F) mess = f'changed {nFiles} files in {toFolder}' - print(mess) + logging.warning(mess) return True def removeUnimacroVchLineInVocolaFiles(self, subDirectory=None): @@ -556,7 +556,7 @@ def removeUnimacroVchLineInVocolaFiles(self, subDirectory=None): if not os.path.isdir(toFolder): mess = f'cannot find Vocola command files directory, not a valid path: {toFolder}' - print(mess) + logging.warning(mess) return mess nFiles = 0 for f in os.listdir(toFolder): @@ -579,7 +579,7 @@ def removeUnimacroVchLineInVocolaFiles(self, subDirectory=None): self.removeUnimacroVchLineInVocolaFiles(F) self.disableVocolaTakesUnimacroActions() mess = f'removed include lines from {nFiles} files in {toFolder}' - print(mess) + logging.warning(mess) return True @@ -621,7 +621,7 @@ def openConfigFile(self): """open the natlink.ini config file """ os.startfile(self.config_path) - print(f'opened "{self.config_path}" in a separate window') + logging.info(f'opened "{self.config_path}" in a separate window') return True def setAhkExeDir(self, arg): @@ -649,8 +649,8 @@ def clearAhkUserDir(self, arg=None): self.clearDirectory(key, section='autohotkey') def printPythonPath(self): - print('the python path:') - pprint(sys.path) + logging.info('the python path:') + logging.info(pformat(sys.path)) def isValidDir(path): From c53e0160ce05b5227cb7cff50fadad34cc77b12c Mon Sep 17 00:00:00 2001 From: Doug Ransom Date: Sat, 17 Feb 2024 08:32:45 -0800 Subject: [PATCH 11/15] logging to cli. --- .../configure/natlinkconfig_cli.py | 29 +++++++++++-------- .../configure/natlinkconfig_gui.py | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/natlinkcore/configure/natlinkconfig_cli.py b/src/natlinkcore/configure/natlinkconfig_cli.py index 466b2a3..9d3a44b 100644 --- a/src/natlinkcore/configure/natlinkconfig_cli.py +++ b/src/natlinkcore/configure/natlinkconfig_cli.py @@ -9,6 +9,22 @@ from pathlib import Path from natlinkcore.configure import natlinkconfigfunctions import logging +appname="natlink" +logdir = Path(user_log_dir(appname=appname,ensure_exists=True)) +logfilename=logdir/f"cli_log.txt" +file_handler = logging.FileHandler(logfilename) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +file_handler.setFormatter(formatter) +logfile_logger = logging.getLogger() + +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +logfile_logger.addHandler(handler) + +file_handler.setLevel(logging.DEBUG) +logfile_logger.addHandler(file_handler) +logfile_logger.setLevel(logging.DEBUG) + def _main(Options=None): """Catch the options and perform the resulting command line functions @@ -18,18 +34,7 @@ def _main(Options=None): etc., usage above... """ - appname="natlinkconfig_cli" - logdir = Path(user_log_dir(appname=appname,ensure_exists=True)) - logfilename=logdir/f"{appname}.txt" - file_handler = logging.FileHandler(logfilename) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler.setFormatter(formatter) - logfile_logger = logging.getLogger() - - #always leave at debug. So we have this if there is ever a problem. - file_handler.setLevel(logging.DEBUG) - logfile_logger.addHandler(file_handler) - logfile_logger.setLevel(logging.DEBUG) + cli = CLI() cli.Config = natlinkconfigfunctions.NatlinkConfig() diff --git a/src/natlinkcore/configure/natlinkconfig_gui.py b/src/natlinkcore/configure/natlinkconfig_gui.py index 8491736..e85d0d4 100644 --- a/src/natlinkcore/configure/natlinkconfig_gui.py +++ b/src/natlinkcore/configure/natlinkconfig_gui.py @@ -6,7 +6,7 @@ import logging from platformdirs import user_log_dir from pathlib import Path -appname="natlinkconfig_gui" +appname="natlink" logdir = Path(user_log_dir(appname=appname,ensure_exists=True)) logfilename=logdir/"config_gui_log.txt" file_handler = logging.FileHandler(logfilename) From 761393e945b68dbe0076c1f8356abaf788094b83 Mon Sep 17 00:00:00 2001 From: Doug Ransom Date: Sat, 17 Feb 2024 08:37:22 -0800 Subject: [PATCH 12/15] tweak. --- src/natlinkcore/configure/natlinkconfigfunctions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/natlinkcore/configure/natlinkconfigfunctions.py b/src/natlinkcore/configure/natlinkconfigfunctions.py index 714453c..e1b2e81 100644 --- a/src/natlinkcore/configure/natlinkconfigfunctions.py +++ b/src/natlinkcore/configure/natlinkconfigfunctions.py @@ -38,7 +38,7 @@ def do_pip(*args): """ Run a pip command with args. - Diagnostic logging. + Diagnostic logging.3 """ From 981733cdd3781bfe69d977cdced1eac306820c10 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Wed, 21 Feb 2024 17:57:41 +0100 Subject: [PATCH 13/15] a few documentation details... --- src/natlinkcore/natlinkstatus.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/natlinkcore/natlinkstatus.py b/src/natlinkcore/natlinkstatus.py index 3ba31d8..5f5745d 100644 --- a/src/natlinkcore/natlinkstatus.py +++ b/src/natlinkcore/natlinkstatus.py @@ -5,9 +5,16 @@ # (C) Copyright Quintijn Hoogenboom, February 2008/January 2018/extended for python3, Natlink5.0.1 Febr 2022 # #pylint:disable=C0302, C0116, R0902, R0904, R0912, W0107, E1101, C0415 -"""The following functions are provided in this module: +"""Normal use: -The functions below are put into the class NatlinkStatus. +``` +from natlinkcore import natlinkstatus +... +status = natlinkstatus.NatlinkStatus() +``` + + +Then the following functions (methods) can be called: The functions below should not change anything in settings, only get information. From 99b196a66721212c33f1bec3d9ef11bb89d82f26 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Wed, 21 Feb 2024 17:58:43 +0100 Subject: [PATCH 14/15] try to catch an error when unloading a grammar, when it is not (correctly) loaded --- src/natlinkcore/natlinkutils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/natlinkcore/natlinkutils.py b/src/natlinkcore/natlinkutils.py index e85b2e8..1dcc1a7 100644 --- a/src/natlinkcore/natlinkutils.py +++ b/src/natlinkcore/natlinkutils.py @@ -312,7 +312,10 @@ def __init__(self): self.grammarName = '' def __del__(self): - self.gramObj.unload() + try: + self.gramObj.unload() + except AttributeError: + pass def load(self, gramSpec, allResults=0, hypothesis=0, grammarName=None): self.grammarName = grammarName or self.grammarName From c94dfe046a0c371a7561fcb77c7c554450f4e853 Mon Sep 17 00:00:00 2001 From: Quintijn Hoogenboom Date: Wed, 21 Feb 2024 18:00:14 +0100 Subject: [PATCH 15/15] adapt load_or_reload_module function with force_load, so it works from Unimacro (_control.py grammar). --- src/natlinkcore/loader.py | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/natlinkcore/loader.py b/src/natlinkcore/loader.py index bf8422d..e562314 100644 --- a/src/natlinkcore/loader.py +++ b/src/natlinkcore/loader.py @@ -325,31 +325,31 @@ def load_or_reload_module(self, mod_path: Path, force_load: bool = False) -> Non return else: maybe_module = self.loaded_modules.get(mod_path) - if force_load or maybe_module is None: + # remove force_load here, in favor of below: + if 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 + + 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.debug(f'skipping unchanged loaded module: {mod_name}') - return + 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 + # 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}')