Archive for June, 2008

twill is mini-language done right

In my college career I found a book whose genre I can best describe as "computer science philosophy." The Pragmatic Programmer taught me several key philosophical ideals behind real-world code design, which I found to be quite useful in my every-day programming life. [*]

One of the aforementioned "Pragmatic Programming" practices that I had the most difficulty wrapping my head around dealt with the creation of mini-languages close to the problem domain (AKA domain specific languages). To me, this seemed like an unnecessary amount of additional work, when one could just specify an interface that was a close approximation of a mini-language in a semantically rich language. After looking at twill, I realize that it isn't too much work, specifically because you can create a close approximation in a semantically rich language.

Twill has a commands module that exports functions with simple names; for example, "go", "find", "back", etc. This is the more simple approximation of a mini-language that I had anticipated as a time savings. However, Twill also has a shell module which deals with the online execution of these functions and their arguments within a shell-like environment. This creates, as interpreted by the shell, a mini-language!

With Twill's elegant and maintainable (extensible) coding, the command loop is approximately 250 lines. It's not too hard to create a simple file reader to interpret commands line by line from a script and feed them into the command loop one by one, at which point you have a scriptable mini-language!

There's nothing extraordinarily amazing about any single component of this process, but the fact that a mini-language can be created with so little effort kind of stunned me — one of those "lightbulb going off in your head" situations. :)

Letting my mind wander a bit, I doubt that it's always possible to wrap an existing API with simple, domain-specific, script-like commands. If creating a domain-specific mini-language is a tangible possibility, it seems like a design decision you want to know early on.

Footnotes

[*]

My CS degree program had a tendency to emphasize provably correct programs, which I don't find great need for in my personal day-to-day work beyond some simple design-by-contract guidelines. This is the topic for another entry, however... perhaps several entries!

How not to do software version numbers

There's a really nice, production-ready package called Pygments available from the Cheese Shop. [*] It's a brilliant package, but the developers made a silly mistake. [†]

The release version numbers progress from 0.9 to 0.10.

Sure, this looks reasonable; especially given that in Python developers typically write their version numbers like the following:

>>> import pygments
>>> if pygments.__version__ < (0, 5):
...     raise ImportError('Pygments too old.')
...
Traceback (most recent call last):
...
ImportError: Pygments too old.

This would be fine progressing from the (0, 9) tuple to the (0, 10) tuple. However, if your clients interpret your version number as a float (which isn't fully unreasonable, given that it really is represented as a float in writing) and you progress from version 0.9 to 0.10, your code base just fell back 8 minor releases, as in the following:

>>> import pygments
>>> print pygments.__version__
0.10

Additionally, you annoy people like me who get errors like this:

2008-06-17 20:43:58,653 Trac[loader] ERROR: Skipping "TracPygments 0.3dev" ("Pygments&gt;=0.5" not found)

When their setup says this:

cdleary$ pygmentize -V
Pygments version 0.10, (c) 2006-2007 by Georg Brandl .

Anybody who looks at numerical data in a sorted directory is familiar with the results of this phenomenon:

cdleary@gamma:~/example$ ls -l
total 0
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 10
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 11
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 7
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 8
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 9

Which is resolved with a simple prefixing method, as follows:

cdleary@gamma:~/example$ ls -l
total 0
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 07
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 08
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 09
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 10
-rw-r----- 1 cdleary cdleary 0 2008-06-17 22:00 11

Now I have the unfortunate choice of either hacking up my installation to allow the release number, or downgrading.

Footnotes

[*]

The Cheese Shop is now called PyPi, but I liked the Monty Python Cheese Shop sketch enough to honor its memory.

[†]

Update: I've lightly edited this post to be more tactful — I was slightly peeved at the time of its writing. :( See Problems with Python version parsing for more insight.

World-readable plaintext passwords and toddler murder

What do world readable plaintext passwords and toddler murder have in common? They're both easy.

Oh, right... not to mention they're both bad! I, for one, have accepted our not-evil corporate overlords and have been using GMail since my full-time-student unbecoming. As a result, I was looking at the GMail notifiers available in the Ubuntu repository.

One, called cgmail, was written in Python and had a fairly beautiful codebase. cgmail tied nicely into gconf and had everything going for it. I totally would be using it if it didn't crash ten times during five minutes of configuration. [*]

Another, called gmail-notify, worked perfectly. The source looked like it was written by a Java programmer (you know, making a "main" method for classes and such) who didn't believe in refactoring or PEP8, which made me a little sad. What made me really sad was finding that it stored my password in plaintext in a word readable file, and I had never gotten any warning.

This is a bug on the part of two parties: the MOTU who maintains this package (I'll be submitting a bug report) and the creator of the program. The Gentoo wiki has a page on the ability to install via portage, from which I quote:

elog "Warning: if you check the 'save username and password' option"
elog "your password will be stored in plaintext in ~/.notifier.conf"
elog "with world-readable permissions. If this concerns you, do not"
elog "check the 'save username and password' option."

Ideally this would read: "There is no 'save username and password' option." Just to recap some things:

  1. Do design your program to allow for plugins that tie into keyring managers,

  2. Don't knowingly put some of my most sensitive data where any user on the system can read it, and

  3. Don't, for God's sake, let me install a program that does this without telling me!

I don't mind that Pidgin stores my password in plaintext because it's an Instant Messaging client and it's as careful as possible to use file permissions as protection. gmail-notify used my default umask, which is clearly not good enough, to protect perhaps the most personal data that I have.

You know who you should really feel sorry for, though? Linux-using toddlers.

Footnotes

[*]

I'll probably end up contributing to this project.

A senseless default - press F1 to continue

In lieu of mandatory manual configuration, many devices ship with a default (AKA "stock") configuration, which may then be altered at the owner's will. This falls under the best practice category of "sensible default" design, where the creator chooses a sensible set defaults in order to convenience a client.

"Keyboard error or no keyboard present; Press F1 to continue" is an example of the failure to exercise this practice: because of a mysterious CMOS reset, my headless server becomes useless.

Java GridLayout can't center extra space

Java's GridLayout design appears to lack some forethought. You can't center the elements that are laid out by Java's GridLayout class if the space cannot be evenly distributed among the number of columns (or rows). You can do left-to-right, right-to-left, top-to-bottom, and/or bottom-to-top, but you cannot center. This seems quite silly.

In the end, I wound up subclassing GridLayout and fixing the mistake. GridBagLayout was an inappropriate alternative, since GridBagLayout relies on the preferredWidth of its constituent elements to lay out a container and I just wanted a grid! A simple grid that centers elements in the container if it can't use up all the space can be found inline:

import java.awt.*;

/**
 * Centers the lopsided extra pixel distribution from GridLayout to within
 * one-half pixel margin of error.
 *
 * @author Chris Leary
 */
public class CenteredGridLayout extends GridLayout {

    public CenteredGridLayout() {}

    public CenteredGridLayout(int rows, int cols) {
        super(rows, cols);
    }

    public CenteredGridLayout(int rows, int cols, int hgap, int vgap) {
        super(rows, cols, hgap, vgap);
    }

    /**
     * @return  3-tuple (starts, perObj, realDims).
     *          starts are starting pixel deltas in relation to the
     *              parent container
     *          perObj = (pixelsPerObjX, pixelsPerObjY)
     *          realDims = (rowsX, colsY); automatically calculated if not
     *              set to 0 on the container
     */
    private int[][] lcHelper(Container parent, int componentCount) {
        /*
         * The available space for actual objects is:
         *  parent.width - leftInset - rightInset - hgap * (cols - 1);
         *
         * Note that the (cols - 1) is because the last item needs no hgap.
         *
         * If the available space isn't evenly divisible into the number of
         * columns, we have to distribute the remainder evenly across the
         * insets.
         *
         * If the remainder isn't evenly divisible into the two insets, the
         * right/bottom inset is given the extra pixel.
         */
        int rows = getRows();
        int cols = getColumns();
        Insets insets = parent.getInsets();
        /* Calculate dimensions if not explicitly given. */
        int realCols = (cols == 0)
            ? (int) Math.ceil(((double) componentCount) / rows)
            : cols;
        int realRows = (rows == 0)
            ? (int) Math.ceil(((double) componentCount) / cols)
            : rows;

        /* Helper values. */
        int hInset = insets.left + insets.right;
        int vInset = insets.bottom + insets.top;
        int parentHeight = parent.getHeight();
        int parentWidth = parent.getWidth();

        /* Distribution calculations. */
        int hGapTotal = (realCols - 1) * getHgap();
        int vGapTotal = (realRows - 1) * getVgap();
        int widthPerItem = (parentWidth - hInset - hGapTotal) / realCols;
        int heightPerItem = (parentHeight - vInset - vGapTotal) / realRows;
        int extraWidth = parentWidth
            - (widthPerItem * realCols + hGapTotal);
        int extraHeight = parentHeight
            - (heightPerItem * realRows + vGapTotal);

        /* Package values in containers for return. */
        int[] starts = { /* x, y */
            insets.left + extraWidth / 2,
            insets.top + extraHeight / 2};
        int[] perObj = { widthPerItem, heightPerItem };
        int[] realDims = { realCols, realRows };
        return new int[][] { starts, perObj, realDims };
    }

    /**
     * Set bounds for objects within parent.
     * @param parent    Container being layed out.
     */
    @Override
    public void layoutContainer(Container parent) {
        synchronized (parent.getTreeLock()) {
            int componentCount = parent.getComponentCount();
            if (componentCount == 0) {
                return; /* Nothing to lay out. */
            }
            /* Unpack data calculated by helper. */
            int[][] params = lcHelper(parent, componentCount);
            int[] starts = params[0];
            int[] perObj = params[1];
            int[] realDims = params[2];
            int realCols = realDims[0];
            int realRows = realDims[1];

            /* Move down the height per object plus vertical gap
             * per row. */
            for (
                int y = starts[1], row = 0;
                row < realRows;
                y += perObj[1] + getVgap(), row++
            ) {
                /* Move over the width per object plus horizontal gap per
                 * row. */
                for (
                    int x = starts[0], col = 0;
                    col < realCols;
                    x += perObj[0] + getHgap(), col++
                ) {
                    int arrayIndex = row * realCols + col;
                    parent.getComponent(arrayIndex)
                        .setBounds(x, y, perObj[0], perObj[1]);
                }
            }
        }
    }
}

Here's an illustration of the normal GridLayout failing to allocate my space evenly, using a left-to-right and top-to-bottom setting:

There are 8 pixels of extra space maximum, since 8 pixels isn't evenly divisible over the nine cells.