07 February 2016

I needed to make a simple GUI for translating comma-separated value input into a reStructuredText table, and ended up writing a simple Python program that might be a useful example for you of Tkinter, tkFileDialog, and a combination command line and GUI program.

What is this for?

I needed a simple program to convert CSV files into reStructuredText tables for a group of people who write in RST and don't want to be bothered to create RST tables by hand (which really is a pain unless you’re using Emacs and its text-based tables package).

I started with the command-line version to get the functionality then added the GUI elements.

def write_table(outputfile, table_contents):
    """ Write out the .rst file with the table in it
    """
    with open(outputfile, "wb") as output_file:
        try:
            output_file.write(tabulate(table_contents,
                                       tablefmt="grid",
                                       headers="firstrow"))
        except:
            return False
    return True


def command_line(args):
    """ Run the command-line version
    """
    if args.output is None:
        args.output = get_output_filename(args.input)

    table_contents = read_csv(args.input)

    if write_table(args.output, table_contents):
        print "rst table is in file `{}'".format(args.output)
    else:
        print "Writing file `{}' did not succeed.".format(args.output)


def read_csv(filename):
    """ Read the CSV file

    This fails pretty silently on any exception at all
    """
    try:
        with open(filename, 'rb') as csvfile:
            dialect = csv.Sniffer().sniff(csvfile.read(1024))
            csvfile.seek(0)
            reader = csv.reader(csvfile, dialect)
            r = []
            for row in reader:
                r.append(row)
    except:
        return None

    return r


def get_parser():
    """ The argument parser of the command-line version """
    parser = argparse.ArgumentParser(description=('convert csv to rst table'))

    parser.add_argument('--input', '-F',
                        help='name of the intput file')

    parser.add_argument('--output', '-O',
                        help=("name of the output file; " +
                              "defaults to <inputfilename>.rst"))
    return parser


if __name__ == "__main__":
    """ Run as a stand-alone script """

    parser = get_parser()       # Start the command-line argument parsing
    args = parser.parse_args()  # Read the command-line arguments

    if args.input:              # If there is an argument,
        command_line(args)      # run the command-line version
    else:
        gui()                   # otherwise run the GUI version

Tkinter

There are many resources for Python’s Tk integration library (Tkinter) on the Internet, but the basics are:

  • create the widgets (buttons, labels, text entry fields, etc.) you want, and use the pack() function to get them arranged.
  • add functions that are called when the buttons are pressed; these are called callback functions because once the loop is started in the next step, the only way out of the loop is to take a brief detour from the loop to the function associated with the widget (typically buttons)
  • call the function mainloop()

As a side note, this is how most graphical user interfaces work; in Microsoft Windows this is called GetMessage(), in Mac OS X it is CFRunLoopRun(), in Android apps it is android.os.Looper.

With a text entry field and a Go button to process the file, this little program could be considered complete.

def gui():
    """make the GUI version of this command that is run if no options are
    provided on the command line"""

    def button_go_callback():
        """ what to do when the "Go" button is pressed """
        input_file = entry.get()
        if input_file.rsplit(".")[-1] != "csv":
            statusText.set("Filename must end in `.csv'")
            message.configure(fg="red")
            return
        else:
            table_contents = read_csv(input_file)
            if table_contents is None:
                statusText.set("Error reading file `{}'".format(input_file))
                message.configure(fg="red")
                return
            output_file = get_output_filename(input_file)
            if write_table(output_file, table_contents):
                statusText.set("Output is in {}".format(output_file))
                message.configure(fg="black")
            else:
                statusText.set("Writing file "
                               "`{}' did not succeed".format(output_file))
                message.configure(fg="red")

    root = Tk()
    frame = Frame(root)
    frame.pack()

    statusText = StringVar(root)
    statusText.set("Enter CSV filename, "
                   "then press the Go button")

    label = Label(root, text="CSV file: ")
    label.pack()
    entry = Entry(root, width=50)
    entry.pack()

    button_go = Button(root,
                       text="Go",
                       command=button_go_callback)
    button_go.pack()

    message = Label(root, textvariable=statusText)
    message.pack()

    mainloop()

This code has two parts:

  1. The main function that creates a text entry field and a Go button then calls mainLoop()
  2. A sub-function (or nested function or inner function) that calls the same functions as the command-line version of the program and updates the status line in the GUI as appropriate.

Using this, however, means you have to know the path to the CSV file and type it in to the text entry box. That is not how most modern applications work, usually there is a file browser…

tkFileDialog

The tkFileDialog presents an OS-native file browsing and selection dialog. This script takes the selected file name and populates the text entry box for the CSV file with the full path to the selected file. The nicest part of this is that it only takes a few lines of Python to do this.

We add a Browse button and another nested function to be its callback function. The callback function simply lets tkFileDialog.askopenfilename() give us the name of the file the user selected from the file browser and then fills in the entry field (cleverly named entry in our program) with the full path and file name.

When the Browse button is pressed the browse_button_callback function is called because the button was created with:

button_browse = Button(root,
                       text="Browse",
                       command=button_browse_callback)

and the file name entry field was created with:

entry = Entry(root, width=50)

then the filename comes from the askopenfilename function in tkFileDialog and is used to populate the text entry field.

def button_browse_callback():
    """ What to do when the Browse button is pressed """
    filename = tkFileDialog.askopenfilename()
    entry.delete(0, END)
    entry.insert(0, filename)

What do we have now?

Now we have pretty simply Python program that:

  • can be run from the command line using standard command line options and offering a help menu when the command line option is --help

    $ ./makersttable.py --help
    usage: makersttable.py [-h] [--input INPUT] [--output OUTPUT]
    
    convert csv to rst table
    
    optional arguments:
      -h, --help            show this help message and exit
      --input INPUT, -F INPUT
                            name of the intput file
      --output OUTPUT, -O OUTPUT
                            name of the output file; defaults to
                            <inputfilename>.rst
    
  • in the absence of command line options, a graphical application is started that allows the user to type in a file name or select one from a file browser, then click Go
  • either option results in the creation of a file with a reStructuredText table in it based on the contents of a file with comma-separated values (CSV) in it

With a simple input file formatted like this:

Header Col 1, Header Col 2, Header Col3
This is the first value, This is the second value, This is the third value
Red, Blue, Green
42, 10, 1

the Python program can be run from the command line like:

$ ./makersttable.py -F test.csv
rst table is in file `test.rst'

and the resulting test.rst file looks like:

+-------------------------+--------------------------+-------------------------+
| Header Col 1            | Header Col 2             | Header Col3             |
+=========================+==========================+=========================+
| This is the first value | This is the second value | This is the third value |
+-------------------------+--------------------------+-------------------------+
| Red                     | Blue                     | Green                   |
+-------------------------+--------------------------+-------------------------+
| 42                      | 10                       | 1                       |
+-------------------------+--------------------------+-------------------------+

Or, the Python program can be run like:

$ ./makersttable.py

and you’ll get a graphical interface that looks like:

makersttable-1.png

Figure 1: Initial GUI Screen

Pressing the Browse button will present a file dialog:

makersttable-2.png

Figure 2: The file browsing dialog box

Selecting a file will populate the entry field:

makersttable-3.png

Figure 3: The selected filename shown in the entry field

And pressing the Go button converts the file; the path to which is in the status message area of the GUI:

makersttable-4.png

Figure 4: The path to rST file is shown in the status area

The Whole Python Program

#!/usr/bin/env python

"""Convert CSV to reStructuredText tables

A command-line and PythonTk GUI program to do a simple conversion from
CSV files to reStructuredText tables

A. Caird (acaird@gmail.com)
2016

"""

import argparse
import csv
from tabulate import tabulate
import tkFileDialog
from Tkinter import *


def get_output_filename(input_file_name):
    """ replace the suffix of the file with .rst """
    return input_file_name.rpartition(".")[0] + ".rst"


def gui():
    """make the GUI version of this command that is run if no options are
    provided on the command line"""

    def button_go_callback():
        """ what to do when the "Go" button is pressed """
        input_file = entry.get()
        if input_file.rsplit(".")[-1] != "csv":
            statusText.set("Filename must end in `.csv'")
            message.configure(fg="red")
            return
        else:
            table_contents = read_csv(input_file)
            if table_contents is None:
                statusText.set("Error reading file `{}'".format(input_file))
                message.configure(fg="red")
                return
            output_file = get_output_filename(input_file)
            if write_table(output_file, table_contents):
                statusText.set("Output is in {}".format(output_file))
                message.configure(fg="black")
            else:
                statusText.set("Writing file "
                               "`{}' did not succeed".format(output_file))
                message.configure(fg="red")

    def button_browse_callback():
        """ What to do when the Browse button is pressed """
        filename = tkFileDialog.askopenfilename()
        entry.delete(0, END)
        entry.insert(0, filename)

    root = Tk()
    frame = Frame(root)
    frame.pack()

    statusText = StringVar(root)
    statusText.set("Press Browse button or enter CSV filename, "
                   "then press the Go button")

    label = Label(root, text="CSV file: ")
    label.pack()
    entry = Entry(root, width=50)
    entry.pack()
    separator = Frame(root, height=2, bd=1, relief=SUNKEN)
    separator.pack(fill=X, padx=5, pady=5)

    button_go = Button(root,
                       text="Go",
                       command=button_go_callback)
    button_browse = Button(root,
                           text="Browse",
                           command=button_browse_callback)
    button_exit = Button(root,
                         text="Exit",
                         command=sys.exit)
    button_go.pack()
    button_browse.pack()
    button_exit.pack()

    separator = Frame(root, height=2, bd=1, relief=SUNKEN)
    separator.pack(fill=X, padx=5, pady=5)

    message = Label(root, textvariable=statusText)
    message.pack()

    mainloop()


def write_table(outputfile, table_contents):
    """ Write out the .rst file with the table in it
    """
    with open(outputfile, "wb") as output_file:
        try:
            output_file.write(tabulate(table_contents,
                                       tablefmt="grid",
                                       headers="firstrow"))
        except:
            return False
    return True


def command_line(args):
    """ Run the command-line version
    """
    if args.output is None:
        args.output = get_output_filename(args.input)

    table_contents = read_csv(args.input)

    if write_table(args.output, table_contents):
        print "rst table is in file `{}'".format(args.output)
    else:
        print "Writing file `{}' did not succeed.".format(args.output)


def read_csv(filename):
    """ Read the CSV file

    This fails pretty silently on any exception at all
    """
    try:
        with open(filename, 'rb') as csvfile:
            dialect = csv.Sniffer().sniff(csvfile.read(1024))
            csvfile.seek(0)
            reader = csv.reader(csvfile, dialect)
            r = []
            for row in reader:
                r.append(row)
    except:
        return None

    return r


def get_parser():
    """ The argument parser of the command-line version """
    parser = argparse.ArgumentParser(description=('convert csv to rst table'))

    parser.add_argument('--input', '-F',
                        help='name of the intput file')

    parser.add_argument('--output', '-O',
                        help=("name of the output file; " +
                              "defaults to <inputfilename>.rst"))
    return parser


if __name__ == "__main__":
    """ Run as a stand-alone script """

    parser = get_parser()       # Start the command-line argument parsing
    args = parser.parse_args()  # Read the command-line arguments

    if args.input:              # If there is an argument,
        command_line(args)      # run the command-line version
    else:
        gui()                   # otherwise run the GUI version