From dab3f1f1f31874e631f429568630cbf2a0b94742 Mon Sep 17 00:00:00 2001 From: MikeTheWatchGuy Date: Sat, 6 Jul 2019 11:51:03 -0400 Subject: [PATCH] LOTS of new doc strings. Renamed some functions to begin with _. Window.Show, ReadNonBlocking, Added giant licensing info --- PySimpleGUI.py | 357 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 248 insertions(+), 109 deletions(-) diff --git a/PySimpleGUI.py b/PySimpleGUI.py index f6b44723..230d927e 100644 --- a/PySimpleGUI.py +++ b/PySimpleGUI.py @@ -1,8 +1,103 @@ #!/usr/bin/python3 -version = __version__ = "4.1.0.5 Unreleased" +version = __version__ = "4.1.0.8 Unreleased" + +# 888888ba .d88888b oo dP .88888. dP dP dP +# 88 `8b 88. "' 88 d8' `88 88 88 88 +# a88aaaa8P' dP dP `Y88888b. dP 88d8b.d8b. 88d888b. 88 .d8888b. 88 88 88 88 +# 88 88 88 `8b 88 88'`88'`88 88' `88 88 88ooood8 88 YP88 88 88 88 +# 88 88. .88 d8' .8P 88 88 88 88 88. .88 88 88. ... Y8. .88 Y8. .8P 88 +# dP `8888P88 Y88888P dP dP dP dP 88Y888P' dP `88888P' `88888' `Y88888P' dP +# .88 88 +# d8888P dP + + + + +# __ __ +# / | / | +# $$ | ______ ______ ______ $$ | +# $$ | / \ / \ / \ $$ | +# $$ | /$$$$$$ |/$$$$$$ | $$$$$$ |$$ | +# $$ | $$ $$ |$$ | $$ | / $$ |$$ | +# $$ |_____ $$$$$$$$/ $$ \__$$ |/$$$$$$$ |$$ | +# $$ |$$ |$$ $$ |$$ $$ |$$ | +# $$$$$$$$/ $$$$$$$/ $$$$$$$ | $$$$$$$/ $$/ +# / \__$$ | +# $$ $$/ +# $$$$$$/ + + +""" +Copyright 2018, 2019 PySimpleGUI.org + +OK, let's get the bullshit out of the way + +This software is available for your use under a MODIFIED LGPL3+ license + +This notice, these first 83 lines of code shall remain unchanged + + # # + ## ## #### ##### # ###### # ###### ##### + # # # # # # # # # # # # # # + # # # # # # # # ##### # ##### # # + # # # # # # # # # # # # + # # # # # # # # # # # # + # # #### ##### # # # ###### ##### + + +888 .d8888b. 8888888b. 888 .d8888b. +888 d88P Y88b 888 Y88b 888 d88P Y88b +888 888 888 888 888 888 .d88P +888 888 888 d88P 888 8888" 888 +888 888 88888 8888888P" 888 "Y8b. 8888888 +888 888 888 888 888 888 888 888 +888 Y88b d88P 888 888 Y88b d88P +88888888 "Y8888P88 888 88888888 "Y8888P" + + +And just what the fuck 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 +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: + 1. GitHub - (http://www.PySimpleGUI.com) currently pointing at: + # https://github.com/PySimpleGUI/PySimpleGUI + 2. 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. + Please don't waste anyone's time by filing an Issue unless you have a genuine copy of the software. + +----------------------------------------------------------------------------------------------------------------- + +I absolutely hate having to include that nonsense, but every word is there for solid reasons. + +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. + +Some general bits of advice: +Upgrade your software! 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://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. + +""" + + + +# do the Python 2 or 3 check so the right tkinter stuff can get pulled in import sys - if sys.version_info[0] >= 3: import tkinter as tk from tkinter import filedialog @@ -10,7 +105,7 @@ if sys.version_info[0] >= 3: from tkinter import ttk import tkinter.scrolledtext as tkst import tkinter.font -else: +else: # Do NOT remove any of these regardless of what your IDE or lint says. They are transformed in the 3 to 2 process import Tkinter as tk import tkFileDialog import ttk @@ -24,19 +119,10 @@ import pickle import calendar import textwrap import inspect -from typing import List, Any, Union +from typing import List, Any, Union, Tuple, Dict from random import randint import warnings -# 888888ba .d88888b oo dP .88888. dP dP dP -# 88 `8b 88. "' 88 d8' `88 88 88 88 -# a88aaaa8P' dP dP `Y88888b. dP 88d8b.d8b. 88d888b. 88 .d8888b. 88 88 88 88 -# 88 88 88 `8b 88 88'`88'`88 88' `88 88 88ooood8 88 YP88 88 88 88 -# 88 88. .88 d8' .8P 88 88 88 88 88. .88 88 88. ... Y8. .88 Y8. .8P 88 -# dP `8888P88 Y88888P dP dP dP dP 88Y888P' dP `88888P' `88888' `Y88888P' dP -# .88 88 -# d8888P dP - g_time_start = 0 @@ -1315,7 +1401,7 @@ class Spin(Element): """ :param values: List[Any] List of valid values - :param initial_value: [Any] Initial item to show in window. Choose from list of values supplied + :param initial_value: (Any) Initial item to show in window. Choose from list of values supplied :param disabled: (bool) set disable state :param change_submits: (bool) DO NOT USE. Only listed for backwards compat - Use enable_events instead :param enable_events: (bool) Turns on the element specific events. Spin events happen when an item changes @@ -1411,7 +1497,9 @@ class Spin(Element): # ---------------------------------------------------------------------- # class Multiline(Element): """ - Multiline Element - Display and read multiple lines of text. This is both an input and output element. + Multiline Element - Display and/or read multiple lines of text. This is both an input and output element. + Other PySimpleGUI ports have a separate MultilineInput and MultilineOutput elements. May want to split this + one up in the future too. """ def __init__(self, default_text='', enter_submits=False, disabled=False, autoscroll=False, border_width=None, @@ -1452,8 +1540,7 @@ class Multiline(Element): self.ChangeSubmits = change_submits or enable_events self.RightClickMenu = right_click_menu self.BorderWidth = border_width if border_width is not None else DEFAULT_BORDER_WIDTH - self.TKText = self.Widget = None # type: tk.scrolledtext.ScrolledText - + self.TKText = self.Widget = None # type: tkst.ScrolledText super().__init__(ELEM_TYPE_INPUT_MULTILINE, size=size, auto_size_text=auto_size_text, background_color=bg, text_color=fg, key=key, pad=pad, tooltip=tooltip, font=font or DEFAULT_FONT, visible=visible) return @@ -1508,14 +1595,14 @@ class Multiline(Element): """ Return current contents of the Multiline Element - :return: (str) current contents of the Multiline Input element + :return: (str) current contents of the Multiline Element (used as an input type of Multiline """ return self.TKText.get(1.0, tk.END) def SetFocus(self, force=False): """ - Moves the focus to this Multiline + Moves the focus (that little blinking cursor) to this Multiline Element :param force: (bool). If True, will call focus_force instead of focus_set """ @@ -1537,8 +1624,7 @@ class Multiline(Element): # ---------------------------------------------------------------------- # class Text(Element): """ - Text - Display some text in the window. Usually this means a single line of text. However, the text can also - be multiple lines. If multi-lined there are no scroll bars. + Text - Display some text in the window. Usually this means a single line of text. However, the text can also be multiple lines. If multi-lined there are no scroll bars. """ def __init__(self, text, size=(None, None), auto_size_text=None, click_submits=False, enable_events=False, @@ -1619,28 +1705,28 @@ T = Text # StatusBar # # ---------------------------------------------------------------------- # class StatusBar(Element): - """ """ - + """ + A StatusBar Element creates the sunken text-filled strip at the bottom. Many Windows programs have this line + """ def __init__(self, text, size=(None, None), auto_size_text=None, click_submits=None, enable_events=False, relief=RELIEF_SUNKEN, font=None, text_color=None, background_color=None, justification=None, pad=None, key=None, tooltip=None, visible=True): """ - :param text: (required) text that is to be displayed in the widget + :param text: Text that is to be displayed in the widget :param size: (w,h) w=characters-wide, h=rows-high :param auto_size_text: True if size should fit the text length - :param click_submits: ???????????????????? - :param enable_events: Turns on the element specific events.(Default = False) - :param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` (Default value = RELIEF_SUNKEN) - :param font: specifies the font family, size, etc - :param text_color: color of the text - :param background_color: color of background - :param justification: justification for data display - :param pad: Amount of padding to put around element - :param key: Used with window.FindElement and with return values to uniquely identify this element to uniquely identify this element + :param click_submits: (bool) DO NOT USE. Only listed for backwards compat - Use enable_events instead + :param enable_events: (bool) Turns on the element specific events. StatusBar events occur when the bar is clicked + :param relief: relief style. Values are same as progress meter relief values. Can be a constant or a string: `RELIEF_RAISED RELIEF_SUNKEN RELIEF_FLAT RELIEF_RIDGE RELIEF_GROOVE RELIEF_SOLID` + :param font: Union[str, tuple] specifies the font family, size, etc + :param text_color: (str) color of the text + :param background_color: (str) color of background + :param justification: (str) how string should be aligned within space provided by size. Valid choices = `left`, `right`, `center` + :param pad: (int, int) or ((int, int),(int,int)) Amount of padding to put around element (left/right, top/bottom) or ((left, right), (top, bottom)) + :param key: (Any) Used with window.FindElement and with return values to uniquely identify this element to uniquely identify this element :param tooltip: (str) text, that will appear when mouse hovers over the element - :param visible: set visibility state of the element (Default = True) - + :param visible: (bool) set visibility state of the element """ self.DisplayText = text @@ -1663,11 +1749,11 @@ class StatusBar(Element): """ Changes some of the settings for the Status Bar Element. Must call `Window.Read` or `Window.Finalize` prior - :param value: ?????????????????????????? - :param background_color: color of background - :param text_color: color of the text - :param font: specifies the font family, size, etc - :param visible: (bool) control visibility of element + :param value: (str) new text to show + :param background_color: (str) color of background + :param text_color: (str) color of the text + :param font: Union[str, tuple] specifies the font family, size, etc + :param visible: (bool) set visibility state of the element """ if value is not None: @@ -4625,39 +4711,38 @@ class Window: disable_minimize=False, right_click_menu=None, transparent_color=None, debugger_enabled=True): """ - :param title: - :param layout: - :param default_element_size: (Default value = DEFAULT_ELEMENT_SIZE) - :param default_button_element_size: - :param auto_size_text: True if size should fit the text length - :param auto_size_buttons: - :param location: (Default = (None)) - :param size: (w,h) w=characters-wide, h=rows-high (Default = (None)) - :param element_padding: - :param margins: (Default = (None)) - :param button_color: button color (foreground, background) - :param font: specifies the font family, size, etc - :param progress_bar_color: (Default = (None)) - :param background_color: color of background - :param border_depth: - :param auto_close: (Default = False) - :param auto_close_duration: (Default value = DEFAULT_AUTOCLOSE_TIME) - :param icon: Icon to display. Filled in with default icon in init (Default value = None) - :param force_toplevel: (Default = False) - :param alpha_channel: (Default value = 1) - :param return_keyboard_events: (Default = False) - :param use_default_focus: (Default = True) - :param text_justification: - :param no_titlebar: (Default = False) - :param grab_anywhere: If True can grab anywhere to move the window (Default = False) - :param location: Location on screen to display - :param resizable: (Default = False) - :param disable_close: (Default = False) - :param disable_minimize: (Default = False) + :param title: (str) The title that will be displayed in the Titlebar and on the Taskbar + :param layout: List[List[Elements]] The layout for the window. Can also be specified in the Layout method + :param default_element_size: Tuple[int, int] (width, height) size in characters (wide) and rows (high) for all elements in this window + :param default_button_element_size: Tuple[int, int] (width, height) size in characters (wide) and rows (high) for all Button elements in this window + :param auto_size_text: (bool) True if Elements in Window should be sized to exactly fir the length of text + :param auto_size_buttons: (bool) True if Buttons in this Window should be sized to exactly fit the text on this. + :param location: Tuple[int, int] (x,y) location, in pixels, to locate the upper left corner of the window on the screen. Default is to center on screen. + :param size: Tuple[int, int] (width, height) size in pixels for this window. Normally the window is autosized to fit contents, not set to an absolute size by the user + :param element_padding: Tuple[int, int] or ((int, int),(int,int)) Default amount of padding to put around elements in window (left/right, top/bottom) or ((left, right), (top, bottom)) + :param margins: Tuple[int, int] (left/right, top/bottom) Amount of pixels to leave inside the window's frame around the edges before your elements are shown. + :param button_color: Tuple[str, str] (text color, button color) Default button colors for all buttons in the window + :param font: Union[str, tuple] specifies the font family, size. Uses one of two font specifications formats + :param progress_bar_color: Tuple[str, str] (bar color, background color) Sets the default colors for all progress bars in the window + :param background_color: (str) color of background + :param border_depth: (int) Default border depth (width) for all elements in the window + :param auto_close: (bool) If True, the window will automatically close itself + :param auto_close_duration: (int) Number of seconds to wait before closing the window + :param icon: Union[str, str] Can be either a filename or Base64 value. + :param force_toplevel: (bool) If True will cause this window to skip the normal use of a hidden master window + :param alpha_channel: (float) Sets the opacity of the window. 0 = invisible 1 = completely visible. Values bewteen 0 & 1 will produce semi-transparent windows in SOME environments (The Raspberry Pi always has this value at 1 and cannot change. + :param return_keyboard_events: (bool) if True key presses on the keyboard will be returned as Events from Read calls + :param use_default_focus: (bool) If True will use the default focus algorithm to set the focus to the "Correct" element + :param text_justification: (str) Union ['left', 'right', 'center'] Default text justification for all Text Elements in window + :param no_titlebar: (bool) If true, no titlebar nor frame will be shown on window. This means you cannot minimize the window and it will not show up on the taskbar + :param grab_anywhere: (bool) If True can use mouse to click and drag to move the window. Almost every location of the window will work except input fields on some systems + :param keep_on_top: (bool) If True, window will be created on top of all other windows on screen. It can be bumped down if another window created with this parm + :param resizable: (bool) If True, allows the user to resize the window. Note the not all Elements will change size or location when resizing. + :param disable_close: (bool) If True, the X button in the top right corner of the window will no work. Use with caution and always give a way out toyour users + :param disable_minimize: (bool) if True the user won't be able to minimize window. Good for taking over entire screen and staying that way. :param right_click_menu: List[List[str]] see "Right Click Menus" for format - :param transparent_color: - :param debugger_enabled: (Default = True) - + :param transparent_color: (str) Any portion of the window that has this color will be completely transparent. You can even click through these spots to the window under this window. + :param debugger_enabled: (bool) If True then the internal debugger will be enabled """ self.AutoSizeText = auto_size_text if auto_size_text is not None else DEFAULT_AUTOSIZE_TEXT @@ -4742,10 +4827,12 @@ class Window: # ------------------------- Add ONE Row to Form ------------------------- # def AddRow(self, *args): - """Parms are a variable number of Elements - - :param *args: + """ + Adds a single row of elements to a window's self.Rows variables. + Generally speaking this is NOT how users should be building Window layouts. + Users, create a single layout (a list of lists) and pass as a parameter to Window object, or call Window.Layout(layout) + :param *args: List[Elements] """ NumRows = len(self.Rows) # number of existing rows is our row number CurrentRowNumber = NumRows # this row's number @@ -4780,8 +4867,9 @@ class Window: # ------------------------- Add Multiple Rows to Form ------------------------- # def AddRows(self, rows): """ + Loops through a list of lists of elements and adds each row, list, to the layout - :param rows: + :param rows: List[List[Elements]] A list of a list of elements """ for row in rows: @@ -4789,9 +4877,12 @@ class Window: def Layout(self, rows): """ + Second of two preferred ways of telling a Window what its layout is. The other way is to pass the layout as + a parameter to Window object. The parameter method is the currently preferred method. This call to Layout + has been removed from examples contained in documents and in the Demo Programs. Trying to remove this call + from history and replace with sending as a parameter to Window. - :param rows: - + :param rows: List[List[Elements]] Your entire layout """ self.AddRows(rows) self.BuildKeyDict() @@ -4799,31 +4890,32 @@ class Window: def LayoutAndRead(self, rows, non_blocking=False): """ - + Deprecated. Now you layout your window's rows (layout) and then separately call Read. :param rows: :param non_blocking: (Default = False) """ raise DeprecationWarning( - 'LayoutAndRead is no longer supported... change your call window.Layout(layout).Read()') + 'LayoutAndRead is no longer supported... change your call window.Layout(layout).Read()\nor window(title, layout).Read()') # self.AddRows(rows) - # self.Show(non_blocking=non_blocking) + # self._Show(non_blocking=non_blocking) # return self.ReturnValues def LayoutAndShow(self, rows): """ - + Deprecated - do not use any longer. Layout your window and then call Read. Or can add a Finalize call before the Read :param rows: """ raise DeprecationWarning('LayoutAndShow is no longer supported... ') - # ------------------------- ShowForm THIS IS IT! ------------------------- # - def Show(self, non_blocking=False): + + def _Show(self, non_blocking=False): """ + NOT TO BE CALLED BY USERS. INTERNAL ONLY! + It's this - :param non_blocking: (Default = False) - + :param non_blocking: (bool) if True, this is a non-blocking call """ self.Shown = True # Compute num rows & num cols (it'll come in handy debugging) @@ -4861,9 +4953,13 @@ class Window: # ------------------------- SetIcon - set the window's fav icon ------------------------- # def SetIcon(self, icon=None, pngbase64=None): """ + Sets the icon that is shown on the title bar and on the task bar. Can pass in: + * a filename which must be a .ICO icon file. + * a bytes object + * a BASE64 encoded file held in a variable - :param icon: - :param pngbase64: + :param icon: (str) Filename or bytes object + :param pngbase64: (str) Base64 encoded GIF or PNG file """ if type(icon) is bytes: @@ -4890,21 +4986,32 @@ class Window: def _GetElementAtLocation(self, location): """ + Given a (row, col) location in a layout, return the element located at that position - :param location: - + :param location: Tuple[int, int] Return the element located at (row, col) in layout + :return: (Element) The Element located at that position in this window """ + (row_num, col_num) = location row = self.Rows[row_num] element = row[col_num] return element def _GetDefaultElementSize(self): - """ """ + """ + Returns the default elementSize + + :return: Tuple[int, int] (width, height) of the default element size + """ + return self.DefaultElementSize def _AutoCloseAlarmCallback(self): - """ """ + """ + Function that's called by tkinter when autoclode timer expires. Closes the window + + :return: None + """ try: window = self if window: @@ -4918,7 +5025,11 @@ class Window: pass def _TimeoutAlarmCallback(self): - """ """ + """ + Read Timeout Alarm callback. Will kick a mainloop call out of the tkinter event loop and cause it to return + + :return: None + """ # first, get the results table built # modify the Results table in the parent FlexForm object # print('TIMEOUT CALLBACK') @@ -4931,17 +5042,21 @@ class Window: def Read(self, timeout=None, timeout_key=TIMEOUT_KEY): """ + THE biggest deal method in the Window class! This is how you get all of your data from your Window. + Pass in a timeout (in milliseconds) to wait for a maximum of timeout milliseconds. Will return timeout_key + if no other GUI events happen first. - :param timeout: timeout to wait, till Read will execute itself - :param timeout_key: (Default value = TIMEOUT_KEY) - + :param timeout: (int) Milliseconds to wait until the Read will return IF no other GUI events happen first + :param timeout_key: (Any) The value that will be returned from the call if the timer expired + :return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values) + (event or timeout_key or None, Dictionary of values or List of values from all elements in the Window """ # ensure called only 1 time through a single read cycle if not Window.read_call_from_debugger: _refresh_debugger() timeout = int(timeout) if timeout is not None else None if timeout == 0: # timeout of zero runs the old readnonblocking - event, values = self.ReadNonBlocking() + event, values = self._ReadNonBlocking() if event is None: event = timeout_key if values is None: @@ -4954,7 +5069,7 @@ class Window: if self.TKrootDestroyed: return None, None if not self.Shown: - self.Show() + self._Show() else: # if already have a button waiting, the return previously built results if self.LastButtonClicked is not None and not self.LastButtonClickedWasRealtime: @@ -5044,8 +5159,13 @@ class Window: self.ReturnValues = self.TimeoutKey, self.ReturnValues[1] # fake a timeout return self.ReturnValues - def ReadNonBlocking(self): - """ """ + def _ReadNonBlocking(self): + """ + Should be never called directly by the user. The user can call Window.Read(timeout=0) to get same effect + + :return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values) + (event or timeout_key or None, Dictionary of values or List of values from all elements in the Window + """ if self.TKrootDestroyed: try: self.TKroot.quit() @@ -5055,7 +5175,7 @@ class Window: # print('DESTROY FAILED') return None, None if not self.Shown: - self.Show(non_blocking=True) + self._Show(non_blocking=True) try: rc = self.TKroot.update() except: @@ -5075,11 +5195,18 @@ class Window: return BuildResults(self, False, self) def Finalize(self): - """ """ + """ + Use this method to cause your layout to built into a real tkinter window. In reality this method is like + Read(timeout=0). It doesn't block and uses your layout to create tkinter widgets to represent the elements. + Lots of action! + + :return: (Window) Returns 'self' so that method "Chaining" can happen (read up about it as it's very cool!) + """ + if self.TKrootDestroyed: return self if not self.Shown: - self.Show(non_blocking=True) + self._Show(non_blocking=True) try: rc = self.TKroot.update() except: @@ -5089,8 +5216,17 @@ class Window: # return None, None return self + def Refresh(self): - """ """ + """ + Refreshes the window by calling tkroot.update(). Can sometimes get away with a refresh instead of a Read. + Use this call when you want something to appear in your Window immediately (as soon as this function is called). + Without this call your changes to a Window will not be visible to the user until the next Read call + + :return: Tuple[(Any), Union[Dict[Any:Any], List[Any], None] (event, values) + (event or timeout_key or None, Dictionary of values or List of values from all elements in the Window + """ + if self.TKrootDestroyed: return self try: @@ -5350,6 +5486,7 @@ class Window: pass # if down to 1 window, try and destroy the hidden window, if there is one if Window.NumOpenWindows == 1: + # print('Trying to destroy hidden') try: Window.hidden_master_root.destroy() Window.NumOpenWindows = 0 # if no hidden window, then this won't execute @@ -8122,7 +8259,6 @@ def StartupTK(my_flex_form: Window): # print('Starting TK open Windows = {}'.format(ow)) if ENABLE_TK_WINDOWS: root = tk.Tk() - Window.IncrementOpenCount() elif not ow and not my_flex_form.ForceTopLevel: # if first window being created, make a throwaway, hidden master root. This stops one user # window from becoming the child of another user window. All windows are children of this @@ -10127,10 +10263,13 @@ class _Debugger(): try: result = eval('{}'.format(cmd), myglobals, mylocals) except Exception as e: - try: - result = exec('{}'.format(cmd), myglobals, mylocals) - except Exception as e: - result = 'Exception {}\n'.format(e) + if sys.version_info[0] < 3: + result = 'Not available in Python 2' + else: + try: + result = exec('{}'.format(cmd), myglobals, mylocals) + except Exception as e: + result = 'Exception {}\n'.format(e) self.watcher_window.Element('_OUTPUT_').Update('{}\n'.format(result), append=True, autoscroll=True) # BUTTON - DETAIL