Saturday, August 18, 2018

UnboundLocalError in Python 3 When Using an Exception Alias

While modifying execsql to run under Python 3 as well as Python 2, I encountered a version compatibility issue that I did not find described in any readily available online documentation for 2-to-3 conversion. It is:

In Python 3, when an exception is aliased to the same name as an existing variable, that variable is destroyed, and subsequent references to the variable will result in an UnboundLocalError exception.

Here is a little code to illustrate that effect:


class SomeError(Exception):
 def __init__(self, msg):
  self.message = msg

def generate_exception():
 return 'five' / 'two'

def test_local():
 er = None
 try:
  generate_exception()
 except TypeError as er:
  pass
 except:
  er = SomeError("Unexpected exception.")
 if er:
  print("An exception occurred.")

test_local()

This runs successfully in Python 2, but an UnboundLocalError is generated in Python 3.6.5 at the line "if er:".

This is documented in the Python bug tracker as issue 26174 for Python 3.4, but as of that version the exception generated was a NameError rather than an UnboundLocalError.

Friday, March 16, 2018

Tech Support Allegory

So I go to my doctor, and I say “Doctor, when I stand up, I get this pain in my chest.”  And the doctor says “Well, let’s see about that.  Does this happen when you stand up from a sitting position?”

“Yes,” I say.

So the doctor sits down in a chair and then stands up.  “Hm,” says the doctor, “I don’t feel anything.  There’s no problem as far as I can tell.”  And he walks out the door.

Sunday, February 4, 2018

Building Tkinter Interfaces with Reusable Components from the TkPane Library

Building graphical user interfaces (GUIs) with Python's Tkinter library usually requires a bit of fiddling not only to get the visual layout correct, but also to link the various Tkinter widgets so that they interact properly. Common interactions among widgets include enabling or disabling one based on the state of another, and accessing the data in one widget from another widget. The network of interactions between widgets in a user interface can require painstaking effort to create and modify, and limits the resuability of those user interface components.

Whereas the TkLayout package simplifies creation of the visual layout of a Tkinter GUI, the TkPane package simplifies the creation, use, and re-use of Tkinter widgets and groups of Tkinter widgets, particularly those that are used to collect or manage data, and that need to interact with other components of the GUI.

The TkPane package provides a TkPane class that can be subclassed to create custom 'pane' objects. Several general-purpose panes are provided in the tkpane.lib module. Pane objects are completely stand-alone, with no inherent dependencies on any other components of the GUI. Pane objects have a standard set of methods, however, by which other panes (or other application code) can enable or disable those panes, or clear their displays. Pane objects also have internal lists of callback functions that are automatically executed when their data are changed to become valid, when their data are changed to become invalid, or when the user leaves the pane (via a keyboard focus change or a mouse movement). Dependencies between panes are easily established with the requires() method. For example, the statement

run_button_pane.requires(input_file_pane)

ensures that the run_button_pane pane will be disabled when the input_file_pane pane contains invalid data, and will be enabled when the input_file_pane pane contains valid data. When one pane enables another, the first pane passes its own data to the second pane.

The type of inter-pane interaction that is enabled by the requires() method may be all that is needed in many cases. If more complex interactions are required, however, callback lists can be modified directly to enable other types of inter-pane activation and data sharing.

When a pane object is instantiated, the constructor method (__init()__) must be passed the Tkinter widget within which the frame will be embedded. Typically that parent widget is a frame. This required argument of pane constructors is identical to the argument that must be passed to the 'build' functions of TkLayout objects. Consequently, TkLayout and TkPane objects work together very well, and the combination of the two practically eliminates all of the fiddling with visual and functional aspects of a GUI when using Tkinter directly.

The following example shows how these two packages can be used together to easily build a Tkinter GUI interface by populating layout elements with panes. This example uses pane classes from tkpane.lib.

try:
    import Tkinter as tk
except:
    import tkinter as tk
import tkpane.lib
import tklayout
import time


# Add a method to the AppLayout class to get a pane: the first child of
# a frame's widgets.
def layout_pane(self, pane_name):
    return self.frame_widgets(pane_name)[0]

tklayout.AppLayout.pane = layout_pane


# Create simple sets of data to display in the TableDisplayPane.
ds1_headers = ["Title", "Author", "Published"]
ds1_data = [["Kon-Tiki: Across the Pacific in a Raft", "Thor Heyerdahl", 1950],
            ["Mawson's Will", "Lennard Bickel", 2000],
            ["Southern Cross to Pole Star - Tschiffely's Ride", "A. F. Tschiffely", 1933],
            ["The Hitchhiker's Guide to the Galaxy", "Douglas Adams", 1979]]

ds2_headers = ["row_id","row_number","long_text","some_date","some_number"]
ds2_data = [
           ["Row 4",4,"Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.","1951-03-19",61.9917173461],
           ["Row 8",8,"Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus.","1977-07-21",34.5729855806],
           ["Row 12",12,"DJs flock by when MTV ax quiz prog.","1983-10-12",2.3773967111]
           ]

def build_message_pane(parent):
    tkpane.lib.MessagePane(parent, "Enter user credentials and an output directory.")

def build_table_pane(parent):
    tkpane.lib.TableDisplayPane(parent, "This is an example message to accompany the data table that is long enough that it should wrap when the window is resized to a relatively small size.", ds1_headers, ds1_data)

def build_button_pane(parent):
    def no_action():
        pass
    tkpane.lib.OkCancelPane(parent, no_action, no_action)

# Lay out the panes
lo = tklayout.AppLayout()
inp_panes = lo.row_elements(["user_pane", "output_pane"], row_weight=0)
app = lo.column_elements(["message_pane", inp_panes, "table_pane", 
                          "button_pane", "status_pane"], 
                          row_weights=[0,0,1,0,0])

root = tk.Tk()
root.title("Demo of the TkPane Package")

# Use an extra frame within the root element with padding to add extra space
# around the outermost app widgets.
appframe = tk.Frame(root, padx=11, pady=11)
appframe.pack(expand=True, fill=tk.BOTH)

lo.create_layout(appframe, app)

lo.build_elements({"message_pane": build_message_pane,
                   "user_pane": tkpane.lib.UserPane,
                   "output_pane": tkpane.lib.OutputDirPane,
                   "table_pane": build_table_pane,
                   "button_pane": build_button_pane,
                   "status_pane": tkpane.lib.StatusProgressPane
                   })

# Get the pane objects for customization.
user_pane = lo.pane("user_pane")
output_pane = lo.pane("output_pane")
button_pane = lo.pane("button_pane")
status_pane = lo.pane("status_pane")
table_pane = lo.pane("table_pane")

# Require a user name and output directory to be entered for the 'OK' button
# to be enabled.
button_pane.requires(user_pane)
button_pane.requires(output_pane)

# Start the demo application with the 'OK' button disabled.
tkpane.en_or_dis_able_all([button_pane])

# Make the user and output directory panes report their status.
user_pane.status_reporter = status_pane
output_pane.status_reporter = status_pane

# Make the buttons change the data and the status bar.
def ok_click(*args):
    status_pane.set_status("OK button clicked.")
    time.sleep(0.5)
    status_pane.set_status("Working...")
    for p in range(10, 110, 10):
        status_pane.set_value(p)
        root.update_idletasks()
        time.sleep(0.5)
    status_pane.set_status("Done.")
    table_pane.display_data(ds2_headers, ds2_data)
button_pane.set_ok_action(ok_click)

def cancel_click(*args):
    status_pane.clear([])
button_pane.set_cancel_action(cancel_click)

# Bind  and  to the buttons.
root.bind("", ok_click)
root.bind("", cancel_click)

# Run the application
root.mainloop()



This code produces a GUI with the initial appearance shown below.  The background of the output directory entry is automatically colored to indicate that it is required but does not contain valid data.



As this example illustrates, the re-usability of pane classes allows Tkinter GUIs to be created with almost no Tkinter code.

The TkPane package can be downloaded from the Python Package Index (PyPI), or installed with pip.

pip install tkpane


The documentation for TkPane is available on ReadTheDocs.

Sunday, January 28, 2018

Building Tkinter Interfaces from the Inside Out

Graphical user interfaces (GUIs) ordinarily consist of a number of different elements, such as those illustrated in Figure 1. Some GUI development toolkits, specifically including Python's Tkinter module, require these elements to be assembled from the outside in. That is, the outermost frame must be defined first, and that first frame then populated with any container elements (e.g., additional frames), and then those second-level frames populated next, and so forth.
Figure 1. Sketch of example GUI.

Such an interface may be easier to describe from the inside out, though, rather than from the outside in. For example, the GUI shown in Figure 1 could be described as:

group_1 ← column_of (topic_selection, date_selection)
group_2 ← row_of (group_1, format_options)
group_3 ← column_of (header, group_2, output_selection, buttons)

The group_3 element then describes the entire GUI, including the nested hierarchy of other elements.

The TkLayout Python package allows GUIs to be built by describing them in just this way. The key to this process is separation of the steps of describing the interface and creating the interface.

To describe the interface, the GUI implementer must first assign a name to each element of the GUI. For the example shown in Figure 1, reasonable names would be "topic_selection", "date_selection", etc. The interface is then described using methods of an AppLayout object as follows:

group_1 = column_elements("topic_selection", "date_selection")
group_2 = row_elements(group_1, "format_options")
group_3 = column_elements("header", group_2, "output_selection", "buttons")


The row_elements and column_elements methods synthesize and return new element names that identify the element groupings that are created—i.e., group_1, group_2, and group_3. After the structure of the GUI has been described, the frames that implement that structure are created with another method of the AppLayout object, as follows:

create_layout(root, group_3)

where the root argument is a Tkinter top level object or other container that will contain the GUI.

The create_layout method creates a series of nested frames, as shown in Figure 2 for the GUI illustrated in Figure 1.


Figure 2. Frame hierarchy for the GUI in Figure 1.

After this set of frames is created, the enclosing frame for each element can be obtained from the AppLayout object using that element's name. The GUI implementer can then populate each of these frames with other GUI widgets as appropriate. The AppLayout class will also take a dictionary of element names and element-construction functions, and run those functions to populate each element's enclosing frame.  The functions that create the elements for each frame require only the enclosing frame object, and do not need to know anything about how the elements are laid out in relation to one another.

Separation of structure description and implementation in this way allows the GUI construction process to be greatly simplified.  Exploration of alternate interface layouts is also simplified: each alternate layout can be described in just a few lines of code, without any changes required to code to populate or arrange the interface elements.

The TkLayout package can be downloaded from the Python Package Index (PyPI) or installed with the command

pip install tklayout

The documentation is available on ReadTheDocs.