Tags

For me, Python modules seem to divide into two basic classes: library modules and application modules. The former contain basic building blocks, but the latter has top-level routines the operating system invokes when it runs the application.

Today I thought I’d post about the application framework I use for all apps.

The general idea is that when the operating system runs an application there are “command line parameters” made available to the application. This allows customizing different runs of the app by parameterizing, rather than modifying the code.

In fact the process has two levels. The operating system runs the Python interpreter, which takes the Python script and intended parameters as its parameters. (That the interpreter sees the script name as its first parameter is why the script name is the first argument passed to the script. The script gets all the arguments the interpreter did.)

After years of writing Python apps that took positional parameters, I finally decided on a scheme like this:

PYTHON.EXE myscript.py test n=42 xdim=500 ydim=500 fname=test.out

In other words I decided to use keyword parameters on the command line. Except for the first one, which is a command verb. Using keywords eliminates positioning, which means all the parameters can be optional.

§

I use a template application framework to start any project. It begins like this:

from sys import argv, exc_info
from traceback import extract_tb
from os import path
from logger import logger, info, debug, trace

BasePath = r'...'
CMND = ''
ARGS = {}

Log = logger('app')
#

I add other imports as required, obviously. BasePath is set appropriately (almost always the directory the script is in, but not necessarily).

As you’ll see below, CMND and ARGS allow for default parameters that can be overridden by run-time parameters. CMND is a string — the first parameter, the command verb. ARGS is a dictionary of keyword parameters.

Log is a logger instance for logging (a topic for another day).

§

After this bit at the top, the application code comes next, whatever that involves. (It’s not uncommon for me to include related functions in one application, so the command verb selects the function to run.)

Jumping to the bottom of the file, the very last part is this:

if __name__ == '__main__':
    print('autorun: %s' % argv[0])
    Log.start(path.join(BasePath,'app.log'))
    Log.level(debug())
    cmd = argv[1] if 1 < len(argv) else CMND
    kwargs = ARGS
    for a in argv[2:]:
        nv = a.split('=', 2)
        kwargs[nv[0]] = (nv[1] if 1<len(nv) else '')
        Log.debug('%s: "%s"' % (nv[0], kwargs[nv[0]]))
    try:
        obj = dispatch(cmd, **kwargs)
        print()
        print(obj)
        Log.info()
        Log.info(obj)
    except:
        etype, evalue, tb = exc_info()
        ts = extract_tb(tb)
        Log.error()
        Log.error('%s: %s' % (etype.__name__,str(evalue)))
        for t in ts[-5:]:
            Log.error('[%d] %s  (%s)' % (t[1], t[2], t[0]))
            Log.error('    %s' % t[3])
        raise
    finally:
        Log.end()
#

The if at the top means Python only executes the code when actually running the script (but not when the module is just imported).

This code starts the log file and processes the command line. The very first parameter, if one exists, becomes cmd (otherwise it takes the value from CMND). Any remaining parameters are processed under the assumption of a name=value format (no spaces allowed). These become the kwargs for the application.

The try block mainly contains a call to dispatch() — I’ll get to that next. The except block does a bit of Python reflection magic to unwind the stack for an error dump.

This code never changes.

Actually there are two minor exceptions.

§

The dispatch() function controls what function actually runs. It does change for each application (and a lot during development as different features and tests are added or removed).

It starts off looking like this:

def dispatch (cmd, **kwargs):
    Log.info('command: %s' % cmd)
    for kw in kwargs:
        Log.info('argument[%s]: %s' % (kw,kwargs[kw]))
    Log.info()
    if cmd == 'main': return do_main(**kwargs)
    if cmd == 'demo': return do_demo(**kwargs)
    if cmd == 'test': return do_test(**kwargs)
    return 'Nothing to do!'
#

In truth, I don’t include the lines for the main and demo verbs anymore. I start with test only and add others as needed.

Part of the intent here is that another executing module can invoke functions in this module by calling the dispatch() function with a command and parameters are required. It’s an alternate way to use the module functionality.

This also offers a nice way to include test functions in library modules.

§

Lastly, the dispatch() calls a do_whatever() function depending on the command verb. (Those functions also offer yet another entry point into the module. They can be called directly.)

As I mentioned, I always have the do_test() function:

def do_test (**kwargs):
    Log.info('test:: %s' % str(list(kwargs.keys())))
    fpath = kwargs['fpath'] if 'fpath' in kwargs else BasePath
    fname = kwargs['fname'] if 'fname' in kwargs else 'test.out'
    Log.info('fpath="%s"' % fpath)
    Log.info('fname="%s"' % fname)
    Log.info()
    #
    # ...test code goes here...
    #
    Log.info()
    return 'Done'
#

I use this as a template to copy-paste a new function, so I’ve included a few lines to pull out two common command line parameters. Pulling out whatever parameters are expected follows the same pattern. I also use it for quick tests involving just a few lines of code. (If something starts growing and turning into something, I copy it off as a new do_whatever() function.)

I put all the do_whatever() functions just above the dispatch() function. These are all intended as brief “launcher” functions. Most of the application code is in various functions and classes above these (and below the code at the very top).

FWIW, I tend to follow the literary programming idea that the structure, the very order, of your code communicates to future maintainers, so my order of data, classes, functions, and so forth is highly structured. You can expect to find certain things in certain places in my code.

§

So that’s my framework. I know Python has a command line arguments library, but I’ve always rolled my own on this one. I like having it exactly the way I like having it.

This has evolved a lot over the years I’ve been using Python, but the current form has been fairly stable for a while now. If you find it of any value, great!