Merge pull request #3992 from PySimpleGUI/Dev-latest

New traceback error handling & messages, The Exec APIs exec command, …
This commit is contained in:
PySimpleGUI 2021-03-02 17:39:21 -05:00 committed by GitHub
commit 1bedfbfc15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 257 additions and 91 deletions

View File

@ -39,21 +39,28 @@ port = 'PySimpleGUI'
"""
Copyright 2018, 2019, 2020 PySimpleGUI.org
Copyright 2018, 2019, 2020, 2021 PySimpleGUI.org
OK, let's get the BS out of the way
Before getting into the details, let's talk about the high level goals of the PySimpleGUI project.
This software is available for your use under a MODIFIED LGPL3+ license
From the inception these have been the project principals upon which it is all built
1. Fun - it's a serious goal of the project. If we're not having FUN while making stuff, then something's not right
2. Successful - you need to be successful or it's all for naught
3. You are the important party - It's your success that determines the success of PySimpleGUI
This notice, these first 100 lines of code shall remain unchanged
If these 3 things are kept at the forefront, then the rest tends to fall into place.
PySimpleGUI is a "system", not just a program. There are 4 components of the "PySimpleGUI system"
1. This software - PySimpleGUI.com
2. The documentation - PySimpleGUI.org
3. Demo Programs - Demos.PySimpleGUI.org
4. Support - Issues.PySimpleGUI.org
This software is available for your use under a LGPL3+ license
This notice, these first 150 lines of code shall remain unchanged
# #
## ## #### ##### # ###### # ###### #####
# # # # # # # # # # # # # #
# # # # # # # # ##### # ##### # #
# # # # # # # # # # # #
# # # # # # # # # # # #
# # #### ##### # # # ###### #####
888 .d8888b. 8888888b. 888 .d8888b.
@ -66,15 +73,9 @@ This notice, these first 100 lines of code shall remain unchanged
88888888 "Y8888P88 888 88888888 "Y8888P"
And just what is that? Well, it's LPGL3+ and these FOUR simple stipulations.
1. These and all comments are to remain in this document
2. You will not post this software in a repository or a location for others to download from:
A. Unless you have made 10 lines of changes
B. A notice is posted with the code that it is not the original code but instead derived from an original
3. Forking is OK and does NOT require any changes as long as it is obvious forked and stated on the page
where your software is being hosted. For example, GitHub does a fantastic job of indicating if a repository
is the result of a fork.
4. The "Official" version of PySimpleGUI and the associated documentation lives on two (and **only** two) places:
In addition to the normal publishing requirements of LGPL3+, these also apply:
1. These and all comments are to remain in the source code
2. The "Official" version of PySimpleGUI and the associated documentation lives on two (and **only** two) places:
1. GitHub - (http://www.PySimpleGUI.com) currently pointing at:
https://github.com/PySimpleGUI/PySimpleGUI
2. PyPI - pip install PySimpleGUI is the customary way of obtaining the latest release
@ -83,22 +84,23 @@ And just what is that? Well, it's LPGL3+ and these FOUR simple stipulations.
Read the Docs (via http://www.PySimpleGUI.org). Currently is pointed at:
https://pysimplegui.readthedocs.io/en/latest/
If you've obtained this software in any other way, then those listed here, then SUPPORT WILL NOT BE PROVIDED.
3. If you use PySimpleGUI in your project/product, a notice of its use needs to be displayed in your readme file
-----------------------------------------------------------------------------------------------------------------
How about having FUN with this package?? Terrible note to begin this journey of actually having fun making
GUI based applications so I'll try to make it up to you.
The first bit of good news for you is that literally 100s of pages of documentation await you. And nearly 200
Demo Programs have been written as a "jump start" mechanism to get your running as quickly as possible.
The first bit of good news for you is that literally 100s of pages of documentation await you.
300 Demo Programs have been written as a "jump start" mechanism to get your running as quickly as possible.
Some general bits of advice:
Upgrade your software! pip install --upgrade --no-cache-dir PySimpleGUI
Upgrade your software! python -m pip install --upgrade --no-cache-dir PySimpleGUI
If you're thinking of filing an Issue or posting a problem, Upgrade your software first
There are constantly something new and interesting coming out of this project so stay current if you can
The FASTEST WAY to learn PySimpleGUI is to begin to play with it, and to read the documentation.
http://www.PySimpleGUI.org
http://Calls.PySimpleGUI.org
http://Cookbook.PySimpleGUI.org
The User Manual and the Cookbook are both designed to paint some nice looking GUIs on your screen within 5 minutes of you deciding to PySimpleGUI out.
@ -1270,12 +1272,12 @@ class Element():
else:
warnings.warn('You cannot Update element with key = {} until the window.read() is called or finalized=True when creating window'.format(self.Key), UserWarning)
if not SUPPRESS_ERROR_POPUPS:
popup_error('Unable to complete operation on element with key {}'.format(self.Key),
_error_popup_with_traceback('Unable to complete operation on element with key {}'.format(self.Key),
'You cannot perform operations (such as calling update) on an Element until:',
' window.read() is called or finalize=True when Window created.',
'Adding a "finalize=True" parameter to your Window creation will likely fix this.',
_create_error_message(),
image=_random_error_emoji())
)
return False
@ -7779,32 +7781,32 @@ class Window:
# ------------------------- Add the elements to a row ------------------------- #
for i, element in enumerate(args): # Loop through list of elements and add them to the row
if type(element) == list:
PopupError('Error creating Window layout',
_error_popup_with_traceback('Error creating Window layout',
'Layout has a LIST instead of an ELEMENT',
'This means you have a badly placed ]',
'The offensive list is:',
element,
'This list will be stripped from your layout' , keep_on_top=True, image=_random_error_emoji()
'This list will be stripped from your layout'
)
continue
elif callable(element) and not isinstance(element, Element):
PopupError('Error creating Window layout',
_error_popup_with_traceback('Error creating Window layout',
'Layout has a FUNCTION instead of an ELEMENT',
'This likely means you are missing () from your layout',
'The offensive list is:',
element,
'This item will be stripped from your layout', keep_on_top=True, image=_random_error_emoji())
'This item will be stripped from your layout')
continue
if element.ParentContainer is not None:
warnings.warn('*** YOU ARE ATTEMPTING TO RESUSE AN ELEMENT IN YOUR LAYOUT! Once placed in a layout, an element cannot be used in another layout. ***', UserWarning)
PopupError('Error creating Window layout',
_error_popup_with_traceback('Error creating Window layout',
'The layout specified has already been used',
'You MUST start witha "clean", unused layout every time you create a window',
'The offensive Element = ',
element,
'and has a key = ', element.Key,
'This item will be stripped from your layout',
'Hint - try printing your layout and matching the IDs "print(layout)"', keep_on_top=True, image=_random_error_emoji())
'Hint - try printing your layout and matching the IDs "print(layout)"')
continue
element.Position = (CurrentRowNumber, i)
element.ParentContainer = self
@ -8442,23 +8444,7 @@ class Window:
closest_key = self._find_closest_key(key)
if not silent_on_error:
print('** Error looking up your element using the key: ', key, 'The closest matching key: ', closest_key)
trace_details = traceback.format_stack()
error_message = ''
file_info_pysimplegui = trace_details[-1].split(",")[0]
for line in reversed(trace_details):
if line.split(",")[0] != file_info_pysimplegui:
error_message = line
break
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0]+'\n'+error_parts[1]+ '\n' + ''.join(error_parts[2:])
if not SUPPRESS_ERROR_POPUPS:
key_message = 'A close key was found: {}'.format(closest_key) if closest_key is not None else 'No key found that resembles your key'
filename = error_parts[0][error_parts[0].index('File ')+5:]
line_num = error_parts[1][error_parts[1].index('line ')+5:]
_error_popup_with_code('Key Error', filename, line_num, key_message, error_message)
_error_popup_with_traceback('Key Error', 'Problem finding your key '+ str(key), 'Closest match = ' + str(closest_key))
if not SUPPRESS_RAISE_KEY_ERRORS:
raise KeyError(key)
element = ErrorElement(key=key)
@ -16926,6 +16912,34 @@ def popup_notify(*args, title='', icon=SYSTEM_TRAY_MESSAGE_ICON_INFORMATION, dis
return SystemTray.notify(title=title, message=message, icon=icon, display_duration_in_ms=display_duration_in_ms, fade_in_duration=fade_in_duration, alpha=alpha, location=location)
def _error_popup_with_traceback(title, *args):
if SUPPRESS_ERROR_POPUPS:
return
trace_details = traceback.format_stack()
error_message = ''
file_info_pysimplegui = None
for line in reversed(trace_details):
if __file__ not in line:
file_info_pysimplegui = line.split(",")[0]
error_message = line
break
if file_info_pysimplegui is None:
_error_popup_with_code(title,None, None, 'Did not find your error info')
return
error_parts = None
if error_message != '':
error_parts = error_message.split(', ')
if len(error_parts) < 4:
error_message = error_parts[0]+'\n'+error_parts[1]+ '\n' + ''.join(error_parts[2:])
if error_parts is None:
print('*** Error popup attempted but unable to parse error details ***')
print(trace_details)
return
filename = error_parts[0][error_parts[0].index('File ')+5:]
line_num = error_parts[1][error_parts[1].index('line ')+5:]
_error_popup_with_code(title, filename, line_num, error_message, *args)
def _error_popup_with_code(title, filename=None, line_num=None, *args):
layout = [[Text('ERROR'), Text(title)],
@ -17644,11 +17658,29 @@ def user_settings():
#####################################################################################################
# Subprocess
# These are the "Exec" API calls. They will start up new subprocesses and do other execution
# related operations. Starting your editor at a specific line number in a specific file needs these
#####################################################################################################
'''
'########:'##::::'##:'########::'######::'##::::'##:'########:'########:
##.....::. ##::'##:: ##.....::'##... ##: ##:::: ##:... ##..:: ##.....::
##::::::::. ##'##::: ##::::::: ##:::..:: ##:::: ##:::: ##:::: ##:::::::
######:::::. ###:::: ######::: ##::::::: ##:::: ##:::: ##:::: ######:::
##...:::::: ## ##::: ##...:::: ##::::::: ##:::: ##:::: ##:::: ##...::::
##:::::::: ##:. ##:: ##::::::: ##::: ##: ##:::: ##:::: ##:::: ##:::::::
########: ##:::. ##: ########:. ######::. #######::::: ##:::: ########:
........::..:::::..::........:::......::::.......::::::..:::::........::
:::'###::::'########::'####::'######::
::'## ##::: ##.... ##:. ##::'##... ##:
:'##:. ##:: ##:::: ##:: ##:: ##:::..::
'##:::. ##: ########::: ##::. ######::
#########: ##.....:::: ##:::..... ##:
##.... ##: ##::::::::: ##::'##::: ##:
##:::: ##: ##::::::::'####:. ######::
..:::::..::..:::::::::....:::......:::
These are the functions used to implement the subprocess APIs (Exec APIs) of PySimpleGUI
'''
def execute_subprocess_nonblocking(command, *args):
@ -17674,37 +17706,96 @@ def execute_subprocess_nonblocking(command, *args):
def execute_command_subprocess(command, *args, wait=False, cwd=None):
if _running_linux():
arg_string = ''
for arg in args:
arg_string += ' ' + str(arg)
sp = subprocess.Popen(str(command) + arg_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
else:
# print('executing subprocess command:',command, 'args:',args)
if args is not None:
expanded_args = ' '.join(args)
sp = subprocess.Popen([command, (expanded_args,)], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
"""
Runs the specified command as a subprocess.
The function will immediately return without waiting for the process to complete running. You can use the returned Popen object to communicate with the subprocess and get the results.
Returns a subprocess Popen object.
:param command: Filename to load settings from (and save to in the future)
:type command: (str)
:param *args: Variable number of arguments that are passed to the program being started as command line parms
:type *args: (Any)
:param wait: If True then wait for the subprocess to finish
:type wait: (bool)
:param cwd: Working directory to use when executing the subprocess
:type cwd: (str))
:return: Popen object
:rtype: (subprocess.Popen)
"""
try:
if _running_linux():
arg_string = ''
for arg in args:
arg_string += ' ' + str(arg)
sp = subprocess.Popen(str(command) + arg_string, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
else:
sp = subprocess.Popen([command, ], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
if wait:
out, err = sp.communicate()
if out:
print(out.decode("utf-8"))
if err:
print(err.decode("utf-8"))
# print('executing subprocess command:',command, 'args:',args)
if args is not None:
expanded_args = ' '.join(args)
sp = subprocess.Popen([command, (expanded_args,)], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
else:
sp = subprocess.Popen([command, ], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
if wait:
out, err = sp.communicate()
if out:
print(out.decode("utf-8"))
if err:
print(err.decode("utf-8"))
except Exception as e:
print('** Error executing subprocess - requires Python 3.6+ **', e)
sp = None
return sp
def execute_py_file(pyfile, parms=None, cwd=None, interpreter_command=None):
"""
Executes a Python file.
The interpreter to use is chosen based on this priority order:
1. interpreter_command paramter
2. global setting "-python command-"
3. the interpreter running running PySimpleGUI
:param pyfile: the file to run
:param parms: parameters to pass on the command line
:param cwd: the working directory to use
:param interpreter_command: the command used to invoke the Python interpreter
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
def execute_py_file(pyfile, parms=None, cwd=None):
if parms is not None:
execute_command_subprocess('python' if _running_windows() else 'python3', pyfile, parms, wait=False, cwd=cwd)
if pyfile[0] != '"' and ' ' in pyfile:
pyfile = '"'+pyfile+'"'
if interpreter_command is not None:
python_program = interpreter_command
else:
execute_command_subprocess('python' if _running_windows() else 'python3', pyfile, wait=False, cwd=cwd)
python_program = pysimplegui_user_settings.get('-python command-', '')
if python_program == '':
python_program = 'python' if _running_windows() else 'python3'
if parms is not None and python_program:
sp = execute_command_subprocess(python_program, pyfile, parms, wait=False, cwd=cwd)
elif python_program:
sp = execute_command_subprocess(python_program, pyfile, wait=False, cwd=cwd)
else:
print('execute_py_file - No interpreter has been configured')
sp = None
return sp
def execute_editor(file_to_edit, line_number=None):
"""
Runs the editor that was configured in the global settings and opens the file to a specific line number.
Two global settings keys are used.
'-editor program-' the command line used to startup your editor. It's set
in the global settings window or by directly manipulating the PySimpleGUI settings object
'-editor format string-' a string containing 3 "tokens" that describes the command that is executed
<editor> <file> <line>
:param file_to_edit: the full path to the file to edit
:type file_to_edit: (str)
:param line_number: optional line number to place the cursor
:type line_number: (int)
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
editor_program = pysimplegui_user_settings.get('-editor program-', None)
if editor_program is not None:
format_string = pysimplegui_user_settings.get('-editor format string-', None)
@ -17714,18 +17805,64 @@ def execute_editor(file_to_edit, line_number=None):
else:
command = _create_full_editor_command(editor_program, file_to_edit, line_number, format_string)
# print('final command line = ', command)
execute_command_subprocess(editor_program, command)
sp = execute_command_subprocess(editor_program, command)
else:
print('No editor has been configured in the global settings')
return
sp = None
return sp
def execute_get_results(subprocess_id):
"""
Get the text results of a previously executed execute call
Returns a tuple of the strings (stdout, stderr)
:param subprocess_id: a Popen subprocess ID returned from a previous execute call
:type subprocess_id: (subprocess.Popen)
:return: Tuple[str, str]
"""
out_decoded = err_decoded = ''
if subprocess_id is not None:
try:
out, err = subprocess_id.communicate()
if out:
out_decoded = out.decode("utf-8")
if err:
err_decoded = err.decode("utf-8")
except Exception as e:
print('Error in execute_get_results', e)
out_decoded = err_decoded = ''
return out_decoded, err_decoded
def execute_file_explorer(folder_to_open=''):
"""
The global settings has a setting called - "-explorer program-"
It defines the program to run when this function is called.
The optional folder paramter specified which path should be opened.
:param folder_to_open: The path to open in the explorer program
:type folder_to_open: str
:return: Popen object
:rtype: (subprocess.Popen) | None
"""
explorer_program = pysimplegui_user_settings.get('-explorer program-', None)
if explorer_program is not None:
sp = execute_command_subprocess(explorer_program, folder_to_open)
else:
print('No file explorer has been configured in the global settings')
sp = None
return sp
def _create_full_editor_command(editor, file_to_edit, line_number, edit_format_string):
"""
The global settings has a setting called - "-editor format string-"
It uses 3 "tokens" to describe how to invoke the editor in a way that starts at a specific line #
<editor> <file>:<line>
<editor> <file> <line>
:param editor:
:param file_to_edit:
@ -17757,6 +17894,16 @@ def _get_editor():
return user_settings_get_entry('-editor program-', global_editor)
'''
'########::'########:'########::'##::::'##::'######::::'######:::'########:'########::
##.... ##: ##.....:: ##.... ##: ##:::: ##:'##... ##::'##... ##:: ##.....:: ##.... ##:
##:::: ##: ##::::::: ##:::: ##: ##:::: ##: ##:::..::: ##:::..::: ##::::::: ##:::: ##:
##:::: ##: ######::: ########:: ##:::: ##: ##::'####: ##::'####: ######::: ########::
##:::: ##: ##...:::: ##.... ##: ##:::: ##: ##::: ##:: ##::: ##:: ##...:::: ##.. ##:::
##:::: ##: ##::::::: ##:::: ##: ##:::: ##: ##::: ##:: ##::: ##:: ##::::::: ##::. ##::
########:: ########: ########::. #######::. ######:::. ######::: ########: ##:::. ##:
........:::........::........::::.......::::......:::::......::::........::..:::::..::
'''
#####################################################################################################
@ -18045,9 +18192,9 @@ class _Debugger():
# # ## ##### # ## ##### # ###### #### # # # # # #
# # # # # # # # # # # # # # # # # # ## #
# # # # # # # # # ##### # ##### #### # # # # # # #
# # ###### ##### # ###### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # ##
# # # # # # # # ##### ###### ###### #### ## ## # # #
# # ###### ##### # ###### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # ##
# # # # # # # # ##### ###### ###### #### ## ## # # #
def _choose_auto_watches(self, my_locals):
ChangeLookAndFeel(COLOR_SCHEME)
@ -18645,13 +18792,30 @@ def main_global_pysimplegui_settings():
'PyDev - <editor> <file>:<line>\n' +\
'IDLE - <editor> <file>\n'
tooltip_file_explorer = 'This is the program you normally use to "Browse" for files\n' + \
'For Windows this is normally "explorer". On Linux "nemo" is sometimes used.'
tooltip_theme = 'The normal default theme for PySimpleGUI is "Dark Blue 13\n' + \
'If you do not call theme("theme name") by your program to change the theme, then the default is used.\n' + \
'This setting allows you to set the theme that PySimpleGUI will use for ALL of your programs that\n' + \
'do not set a theme specifically.'
layout = [[T('Global PySimpleGUI Settings', font='DEFAIULT 18')],
[T('Editor Program', size=(20,1)), In(settings.get('-editor program-', ''),k='-EDITOR PROGRAM-', enable_events=True), FileBrowse()],
[T('String to launch your editor to edit at a particular line #. Use <editor> <file> <line> to specify')],
[T('the string that will be executed to edit python files using your editor')],
[T('Edit Format String (hover for tooltip)',tooltip=tooltip), In(settings.get('-editor format string-', '<editor> <file>'),k='-EDITOR FORMAT-', tooltip=tooltip)],
[T('Default Theme For All Programs:'), Combo([''] + theme_list(), settings.get('-theme-', None), k='-THEME-')],
[T('Python Interpreter (normally leave blank)', font='_ 16')],
[T('Command to run a python program:'), In(settings.get('-python command-', ''),k='-PYTHON COMMAND-', enable_events=True), FileBrowse()],
[T('Editor Settings', font='_ 16')],
[T('Command to invoke your editor:'), In(settings.get('-editor program-', ''),k='-EDITOR PROGRAM-', enable_events=True), FileBrowse()],
[T('String to launch your editor to edit at a particular line #.')],
[T('Use tags <editor> <file> <line> to specify the string')],
[T('that will be executed to edit python files using your editor')],
[T('Edit Format String (hover for tooltip)',tooltip=tooltip), In(settings.get('-editor format string-', '<editor> <file>'),k='-EDITOR FORMAT-', tooltip=tooltip)],
[T('File Explorer Program', font='_ 16', tooltip=tooltip_file_explorer)],
[In(settings.get('-explorer program-', ''),k='-EXPLORER PROGRAM-', tooltip=tooltip_file_explorer)],
[T('Theme', font='_ 16')],
[T('Default Theme For All Programs:'), Combo([''] + theme_list(), settings.get('-theme-', None), k='-THEME-', tooltip=tooltip_theme)],
[B('Ok', bind_return_key=True), B('Cancel')],
]
@ -18663,7 +18827,9 @@ def main_global_pysimplegui_settings():
if event == 'Ok':
new_theme = OFFICIAL_PYSIMPLEGUI_THEME if values['-THEME-'] == '' else values['-THEME-']
pysimplegui_user_settings.set('-editor program-', values['-EDITOR PROGRAM-'])
pysimplegui_user_settings.set('-explorer program-', values['-EXPLORER PROGRAM-'])
pysimplegui_user_settings.set('-editor format string-', values['-EDITOR FORMAT-'])
pysimplegui_user_settings.set('-python command-', values['-PYTHON COMMAND-'])
pysimplegui_user_settings.set('-theme-', new_theme)
window.close()
return True
@ -18962,8 +19128,8 @@ I hope you are enjoying using PySimpleGUI whether you sponsor the product or not
]
tab1 = Tab('Graph', frame6, tooltip='Graph is in here', title_color='red')
tab2 = Tab('Multiple/Binary Choice Groups', [[Frame('Multiple Choice Group', frame2, title_color='green', tooltip='Checkboxes, radio buttons, etc', vertical_alignment='t'),
Frame('Binary Choice Group', frame3, title_color='#FFFFFF', tooltip='Binary Choice', vertical_alignment='t'), ]], )
tab2 = Tab('Multiple/Binary Choice Groups', [[Frame('Multiple Choice Group', frame2, title_color='green', tooltip='Checkboxes, radio buttons, etc', vertical_alignment='t', pad=(0,0)),
Frame('Binary Choice Group', frame3, title_color='#FFFFFF', tooltip='Binary Choice', vertical_alignment='t'), ]], pad=(0,0))
# tab3 = Tab('Table and Tree', [[Frame('Structured Data Group', frame5, title_color='red', element_justification='l')]], tooltip='tab 3', title_color='red', )
tab3 = Tab('Table and Tree', [[Column(frame5, element_justification='l', vertical_alignment='t')]], tooltip='tab 3', title_color='red', )
tab4 = Tab('Variable Choice', [[Frame('Variable Choice Group', frame4, title_color='blue')]], tooltip='tab 4', title_color='red', )