Tutorial 10: Global

Right now, the todo items are “locked up” inside the todo function:

@app
def todo():
    # ... snip ...
    yield

    item_list = []
    if os.path.exists(args.file):
        with open(args.file) as f:
            item_list = json.load(f)

add, list, and done all need to access/modify item_list. How?

This is a common need for applications. The top-level app object reads data/configuration/etc, or opens a database connection, or sets up a client for a remote service – or whatever – and the subcommands use those “handles” to do their work.

clik provides a “global” object, g, to facilitate passing around global data/connection handles/etc:

from clik import app, args, g, parser  # note the g

@app
def todo():
    # ... snip ...
    yield

    g.item_list = []
    if os.path.exists(args.file):
        with open(args.file) as f:
            g.item_list = json.load(f)

# ... snip ...

@todo(name='list', alias='ls')
def list_():
    """Show the items on the list."""
    yield
    for i, item in enumerate(g.item_list):
        print('%i. %s' % (i, item))

Assuming the test.json file from before (with the following contents)…

[
  "Pick up nails from hardware store",
  "Grab milk from the grocery",
  "Clean up the kitchen",
  "Feed the cats"
]

list now prints our 0-indexed list of items:

$ ./todo -f test.json list
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

Under the covers, g is just a dictionary that allows you to access values using attributes instead of brackets. The following sets of operations are identical:

g.foo = 'bar'
g['foo'] = 'bar'

g.foo
g['foo']

del g.foo
del g['foo']

We already know we’ll need to print the 0-indexed list output inside done, so let’s factor it out into a function:

def print_list():
    for i, item in enumerate(g.item_list):
        print('%i. %s' % (i, item))

# ... snip ...

@todo(name='list', alias='ls')
def list_():
    """Show the items on the list."""
    yield
    print_list()

Since we’re thinking about it, let’s go ahead and implement done:

import sys

# ... snip ...

@todo
def done():
    # ... snip ...
    yield

    if args.all:
        del g.item_list[:]
    else:
        index = args.index
        while index is None:
            print()
            print_list()
            print()
            selection = input('Item number to remove? ')
            try:
                index = int(selection)
            except ValueError:
                print('error: not an integer:', selection, file=sys.stderr)
        if -1 < index < len(g.item_list):
            del g.item_list[index]
        else:
            print('error: index out of range:', index, file=sys.stderr)

    print()
    print('Updated list:')
    print_list()

This is all straightforward Python code; going over the details of the implementation is beyond the scope of this tutorial.

add is simpler and shorter than done:

@todo
def add():
    # ... snip ...
    yield
    item = args.item
    if item is None:
        item = input('Item to add: ') or None
    if item:
        g.item_list.append(item)
        print()
        print('Updated list:')
        print_list()
    else:
        print('error: empty item', file=sys.stderr)

Playing with the new commands and the test.json file, we see that things are generally working. Changes are not persisted to disk, but we’ll tackle that problem in the next step.

$ ./todo -f test.json ls
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

$ ./todo -f test.json add "Hang picture on the wall"

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats
4. Hang picture on the wall

$ ./todo -f test.json add
Item to add: Hang picture on the wall

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats
4. Hang picture on the wall

$ ./todo -f test.json add ""
error: empty item

$ ./todo -f test.json add ""
Item to add:
error: empty item

$ ./todo -f test.json ls
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

$ ./todo -f test.json done -i 2

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Feed the cats

$ ./todo -f test.json done -a

Updated list:

$ ./todo -f test.json done

0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

Item number to remove? 3

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen

$ ./todo -f test.json done -i 10
error: index out of range: 10

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

$ ./todo -f test.json done

0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

Item number to remove? foo
error: not an integer: foo

0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

Item number to remove? 12
error: index out of range: 12

Updated list:
0. Pick up nails from hardware store
1. Grab milk from the grocery
2. Clean up the kitchen
3. Feed the cats

Nice! The application has really started to take shape. Next we’ll save the changes to disk using cleanup code in the app function.

Note

g (along with the magic parser and args variables) is the other design decision experienced Pythonistas might (rightfully) sneer at. Global variables are generally discouraged in Python, and g actively encourages their use (even if veiled behind a not-technically-a-global-depending-on-how-you-look-at-it proxy object).

The justification is the same as for parser / args. This “g pattern” is one I’ve used extensively (in clik and in Flask) and, while it may be against the Zen of Python, it’s damn useful. Used judiciously, it can be a real boon to productivity and overall code clarity.