It’s not something I use too terribly often, but today I found myself missing the KDE color picker applet, a tiny applet that gives you an eye dropper you can use to quickly sample any color on your screen and copy its hex value to the clipboard. I could have sworn it was available in Kubuntu Gutsy by default. Turns out that, in Hardy at least, it comes as part of the kicker-applets package.
July 29, 2008
Dude, where’s my color picker?
April 27, 2008
Eclipse security component errors
Just got through installing eclipse on an Kubuntu Hardy box, and got the following error during the splash screen:
Could not initialize the application’s security component. The most likely cause is problems with files in your application’s profile directory. Please check that this directory has no read/write restrictions and your hard disk is not full or close to full. It is recommended that you exit the application and fix the problem. If you continue to use this session, you might see incorrect application behaviour when accessing security features.
The solution seems to be to create the directory: ~/.mozilla/eclipse
The corresponding bugs where the above solution was found:
March 5, 2008
Bill Gates at UT
Bill Gates was on The University of Texas at Austin campus a couple weeks ago to talk about "Software, Innovation, Entrepreneurship, and Giving Back." While unable to get a ticket to see the talk in-person, I did watch the live webcast (archive now available). Below is my summary of his talk with a couple of my own comments mixed in.
Bill opened his talk with the mentioning of the incredible advancement of computing. How computers are getting smaller, how the number of transistors on processors are doubling, how more natural computing interfaces are on the way. He talked about how TV delivered over the internet is going to revolutionize that field with a level of personalization and interactivity not seen before. He mentioned how the IT field will be changing as much of the system administration work done by humans today will be taken over by software systems that monitor and fix the systems automatically.
Bill talked about how the software industry is still in its infancy and that while user interface tools, such as touch screens and tablet pens, are cheap, the software to effectively make use of these tools is still very far behind. Now, if you are a fan of Ray Kurzweil and The Age of Spiritual Machines, as I am, this might not surprise you. In his book, Ray details the exponential advancement of computers and mentions that while we might have computers with the computing power equal to that of a human brain by 2020, the software to utilize that power will likely lag by at least a decade.
Bill also talked about how computing and software is becoming a more necessary tool for all branches of science, and played a short video showing visualization of mouse-brain scans. Research today is requiring increasingly large amounts of data and computation, and Bill mentioned that he would like to get more of Microsoft's software into the hands of university researchers. He announced a program called DreamSpark, which allows students free (as in beer) access to Microsoft development software, including Visual Studio and Windows Server. However, at the end of the talk when a student asked Bill about the openness and availability of Microsoft's 3D visualization software to the world of academia, Bill responded that Microsoft's products might not fit all of their needs but that Microsoft wants to make sure that all of their software available to researchers. So Bill says that he wants to make this software available, yet if I go visit the DreamSpark website I see only mainstream products like Visual Studio and nothing at all that looks aimed at helping university researchers.
Another interesting topic was Bill's discussion about Microsoft's research department and how the Bill & Melinda Gates Foundation is helping people in poor countries. Bill talked about how he would like to see computing become available to everyone, including the poorest two billion people of the world. Traditional products and business models for the rich countries of the world are no good in the poor countries of the world. For example, personal computers, no matter how cheap you sell them for, are useless in places with zero literacy, training, networking, and electrical power. Starting this summer, these kinds of issues are what Bill will be focusing his attention on, as earlier in the talk he announced that he will be shifting his work schedule to work full time for the foundation and part time for Microsoft.
Bill stated that medical research for issues faced by the poor countries of the world is currently in an extremely sad condition. There are millions of children per year that die from diseases that are non-existent in the rich third of the world's countries, yet no one is willing to help because there is no profit to be made. For example, ten times more money goes towards researching baldness than towards researching Malaria, which, unlike baldness, kills millions each year. Bill mentioned that with no money driving innovation in these areas, it is going to take government policies, philanthropy, and people with values to take the initiative.
Bill went on to talk about a specific instance of how groups at Microsoft are making a difference. Ideas such as "Digital Green" where Microsoft is going around to small towns making videos of the successful farmers. TVs and DVD players are then brought to the town centers where others can learn techniques from the successful farmers. Bill mentions that this "Farmer Idol" has created a healthy competition between the farmers, where everyone wants to outdo the others so they can be included in the video. The program has resulted in significant productivity gains for these communities.
Bill concluded his speech by telling the audience of computer science and electrical engineering students to help keep their awareness of the world's poorest third of the population and that with some special effort, they can help with the problems those people are facing.
Afterwards his speech, Bill answered several questions about open source software, the decreasing number of people entering the computer science field, the work of his foundation, software in micro devices, and Microsoft’s bid for Yahoo.
Again, the video of the speech is now available, so check it out if you are interested.
December 1, 2007
Installing the latest version of Bazaar and tools on Gentoo
Currently, the bzr-svn and bzr-gtk plugins are not available in Gentoo’s package directory. Fortunately, there is exists a bzr Gentoo overlay. If you’ve never used an overlay before, it’s a way to override or add packages to Gentoo’s standard package directory. In this overlay there are a couple common plugins, as well as a patched version of Subversion that is needed for the bzr-svn plugin.
Current packages in the overlay:
- bzr
- bzrtools
- bzr-config
- bzr-dbus
- bzr-gtk
- bzr-rebase
- bzr-svn
- bzr-vimdiff
- paramiko
- subversion (patched version needed for bzr-svn)
Steps for using the overlay:
- As usual, update your portage tree
emerge --sync
- Make sure you have the following line in your
/etc/make.conf
source /usr/portage/local/layman/make.conf
- Get a copy of the overlay from launchpad
bzr branch lp:///bzr-gentoo-overlay
- Make sure you have the path to the overlay set in
/usr/portage/local/layman/make.conf
PORTDIR_OVERLAY="/path/to/bzr-gentoo-overlay"
- Now you should be able to update your system, pulling in updates to any currently installed packages from the new overlay.
emerge -uav world
- emerge any of the new packages you want that are now available from the overlay. Note: If you install
bzr-svn, make sure to re-install Subversion so that the patched version is used from the overlay.
emerge -av bzr-gtk bzr-svn subversion
September 3, 2007
Texas Python Regional Unconference
Just a quick announcement for all the Houston and surrounding area Python lovers out there. There is a Texas Python Regional Unconference being held in a couple weeks (Sept. 15 – 16) at the University of Houston main campus. Registration is free and as simple as adding your name to the registration wiki page. I’ll be there to talk or hack Django with anyone interested. See you there!
January 11, 2007
Hacking Django, how Bazaar
A short while ago, I went on a mission to find a better way to hack Django. I needed a way to keep my patches organized and updated. So, in this article I discuss my experiences with a tool I found to help me achieve these goals…
Enter Bazaar, one of the several distributed version control systems that have been getting some hype recently. With so many to choose from, why did I choose Bazaar? Well, maybe it was because of the cool name, or maybe it was the pretty website, or maybe just because it’s written in the language I love — Python. If you are familiar with Subversion, then you will feel right at home with Bazaar. This is because Bazaar uses most of the same commands as Subversion (e.g. svn ci = bzr ci, svn st = bzr st, etc.).
Following this guide for hacking an open source project using bzr, I created a django directory to stash my django hacks. After using svn to check out the latest Django code to a directory named upstream, and turning upstream into a bzr repository too, it was time to start the real work. To re-iterate in command line speak, here is what I did:
Update: The steps below have been updated to reflect changes with bzr commands (versions >=0.15rc2).
mkdir django
cd django
bzr init-repo . # use "bzr init-repo --trees ." here instead if using a version <0.15rc2
svn co http://code.djangoproject.com/svn/django/trunk/ upstream
cd upstream
bzr init
echo ".svn" > .bzrignore # do not store svn metadata in our bzr repo
bzr add
bzr ci -m 'Initial import of Django.'
Usually, when you want to add a new feature to some code, you create a branch. The purpose of these, so called, feature branches is to keep the features separated. Trying to create multiple features in the same branch would just be a big mess. Creating branches in a Subversion repository is easy enough, you just svn cp the code to a new location. It’s the keeping your branch in sync with the trunk that’s not as easy.
With Subversion, you have to keep track of the revisions that have already been merged to your branch and make sure to only merge the changes that haven’t already been merged. Not so with Bazaar; Bazaar keeps track of this for you. A simple bzr merge will pull in upstream changes that have not been merged yet.
So, when I wanted to start working on a patch for ticket #2473, I created a branch:
bzr branch upstream in-and-empty-list-2473
After cd‘ing into my new branch and committing my changes, I created a diff to attach to the ticket with the command:
bzr diff -r branch:../upstream
So far, working with Bazaar has been a breeze. Without it, my Django holiday hacking wouldn’t have been nearly as fun.
September 23, 2006
is_authenticated() vs. is_anonymous()
A warning for those who might not of noticed the change. About a month ago (in [3360]), an is_authenticated method was added to the User and AnonymousUser classes. These are the classes used for Django’s default authentication system.
Previously, the template code used for displaying content based on whether or not a user had authenticated went something like:
{% if not user.is_anonymous %}
Content for logged in users.
{% else %}
Content for non-logged in users.
{% endif %}
The problem with this code is that if, somehow, the user template variable did not get populated, your template would treat the requesting user as if they were logged in.
Notice that this sort of behavior will be seen for all negative if statements. The template code {% if not variable %} will always evaluate to True if variable doesn’t exist in the context. This is because Django uses the settings.TEMPLATE_STRING_IF_INVALID value for non-existent template variables; by default, TEMPLATE_STRING_IF_INVALID is '', which evaluates to False (and not False is True).
Notice also that even using the template code:
{% if user.is_anonymous %}
Content for non-logged in users.
{% else %}
Content for logged in users.
{% endif %}
(without the "not" ) will not work in this case because if the user variable is non-existent, the requesting user will still be treated as if they were authenticated.
The new recommended way for checking that a requesting user has been authenticated in your templates is:
{% if user.is_authenticated %}
Content for logged in users.
{% else %}
Content for non-logged in users
{% endif %}
Here, if the user template variable were to not exist your template would treat the requesting user as non-authenticated, just as we would want.
August 26, 2006
Using CrackLib to require stronger passwords
Let’s face it, humans are not well adapted to memorizing strings of random characters; and hence, the average computer user is not very good at creating secure passwords. Most users create passwords made up of easy-to-remember words, like the name of a favorite sports team or maybe the name of a significant other. In this article I will show you how you can make Django require better passwords from your users.
Here are the packages you will need:
Installing the required packages
Step 1: Install CrackLib.
CrackLib is a library for checking that passwords are not easily crackable, or in other words, it makes sure that a password is not based on a simple character pattern or on a dictionary word. CrackLib is a common package that you should be able to find in your distribution’s package manager (or, quite possibly, it could already be installed). Alternatively, you could download the source and follow the installation instructions in the README file. By default, CrackLib installs a python package named cracklib, but it does not have as many features as python-crack. (Redhat does not include the cracklib python package in its cracklib package.)
Step 2 (optional, but recommended): Install extra word dictionaries.
Packaged with CrackLib is a file name cracklib-small. (Some distributions, like RedHat, don’t include this file in their cracklib package, in which case keep reading…) This file is a dictionary of words, simply a long list of words with one word per line. These 50,000 words are a good start, but we can do better. On the CrackLib download page, there is also a package named cracklib-words. After downloading and extracting the package, you will have a single file containing 1,648,379 “words”. Many distributions also have a cracklib-dicts or cracklib-words package that maybe the same or similar to the cracklib-words file on the CrackLib website.
For even more dictionaries, take a look at these word lists. It might also be a good idea to create your own list of words. For example, if you work for a company in the financial industry, it would be a good idea to make a list of words and slang specific to that industry. If you will be dealing with non-english speaking users, it would be a good idea to find some dictionaries in other languages.
Step 3: Create the word indexes that get used by CrackLib.
Now that we have our dictionaries, we need to compile them into an index that CrackLib uses. So, put all your dictionaries in one directory (/usr/share/dict/ is a common place). Depending on your distribution, there will be different commands to run. On gentoo (or if you installed from source), the following (as root) should do the trick:
create-cracklib-dict /usr/share/dict/*
And on RedHat:
mkdict /usr/share/dict/* | packer /usr/lib/cracklib_dict
These commands should create the following files:
/usr/lib/cracklib_dict.hwm
/usr/lib/cracklib_dict.pwd
/usr/lib/cracklib_dict.pwi
Step 4: Install python-crack
If your distribution’s package manager doesn’t include python-crack, then you will have to download and install it yourself. Neither Gentoo nor RedHat include this package, so I will walk you through the install. Download and unpack python-crack. Now, cd into the directory and run
./configure
By default, configure will setup python-crack’s files to be installed to /usr/local. This means that you will have to add /usr/local/lib/python2.4/site-packages to your PYTHONPATH. Alternatively, you could add a --prefix=/usr to the end of the configure command to install python-crack’s files to the usual /usr/lib/python2.4/site-packages directory.
If you get this error:
checking for prefix of the default cracklib dictionary database… unknown
configure: error: crack.h does not define CRACKLIB_DICTPATH. Please use DEFAULT_DICTPATH, see ./configure –help
then set the DEFAULT_DICTPATH variable when running configure, like so
./configure DEFAULT_DICTPATH=/usr/lib/cracklib_dict
Now run
make
make install
Now that we are finished installing, lets test out python-crack
$ python
Python 2.4.3 (#1, Jun 28 2006, 23:41:37)
[GCC 3.4.6 (Gentoo 3.4.6-r1, ssp-3.4.5-1.0, pie-8.7.9)] on linux2
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import crack
>>> crack.VeryFascistCheck(‘foo’)
[snip]
ValueError: it is WAY too short
>>> crack.VeryFascistCheck(‘foobar’)
[snip]
ValueError: it is based on a dictionary word
>>> crack.VeryFascistCheck(‘3#hsad2U>u2u’)
‘3#hsad2U>u2u’
VeryFascistCheck() raises a value error and prints an explaination if the password is not strong enough, otherwise it echos back the password.
Creating the Django Manipulator
from django import forms
class NewUserForm(forms.Manipulator):
def __init__(self):
self.fields = [
forms.TextField(field_name="username", length=20, maxlength=20, is_required=True),
forms.PasswordField(field_name="password", length=20, maxlength=50, is_required=True,
validator_list=[isStrongPassword]),
forms.PasswordField(field_name="confirm_password", length=20, maxlength=50, is_required=True,
validator_list=[forms.validators.RequiredIfOtherFieldGiven("password", "You must confirm password"),
forms.validators.AlwaysMatchesOtherField("password", "Passwords did not match"),]),
]
def isStrongPassword(field_data, all_data):
"""Test the password with cracklib to make sure it is strong."""
import crack
# Increase the number of credits required from the default of 8 if you want.
#crack.min_length = 11
try:
crack.VeryFascistCheck(field_data)
except ValueError, message:
raise forms.validators.ValidationError, "Password %s." % str(message)[3:]
Notice that I added isStrongPassword to the password field’s validator_list. Now you are ready to put the NewUserForm manipulator to use in your you views.py. After running the manipulator’s get_validation_errors(), bad passwords will generate errors like “Password is based on a dictionary word” or “Password does not contain enough DIFFERENT characters.” For help on using manipulators in your view, see the Forms, fields, and manipulators documentation.
If you want to make Django’s built-in authentication require stronger passwords, then you could add validator_list=[isStrongPassword] to the password field of django.contrib.auth.models.User.
Now, go forth and require strong passwords.
July 9, 2006
Cyclomatic complexity of Django
Adrian’s recent post on the dev mailing list about the code within django.db.models got me remembering some other “monstrous” functions I’ve seen while browsing the Django source. Guess no more as to the most monstrous functions of Django because below are the results of my previously posted complexity.py script when run on the Django source.
Adjust your window until you can see all of the following line:
#===============================================================================================================#
Here are all 80 of the functions and methods of Django that are of moderate risk or higher:
Showing functions/methods with complexity greater than or equal to 11: Filename Function/Method Lines of Code Complexity ./core/management.py get_validation_errors 146 78 ./db/models/manipulators.py AutomaticManipulator.save 97 43 ./contrib/comments/templatetags/comments.py DoCommentForm.__call__ 51 31 ./views/i18n.py javascript_catalog 68 28 ./contrib/admin/views/main.py change_stage 75 28 ./utils/translation/trans_real.py translation 62 26 ./bin/make-messages.py make_messages 114 26 ./contrib/admin/views/main.py _get_deleted_objects 65 25 ./core/handlers/base.py BaseHandler.get_response 50 24 ./views/defaults.py shortcut 40 23 ./contrib/auth/create_superuser.py createsuperuser 64 23 ./core/management.py inspectdb 84 23 ./contrib/admin/views/doc.py model_detail 64 22 ./templatetags/i18n.py do_block_translate 41 21 ./core/management.py syncdb 61 21 ./core/management.py execute_from_command_line 74 21 ./views/generic/create_update.py update_object 54 20 ./contrib/admin/views/decorators.py staff_member_required 44 20 ./utils/simplejson/decoder.py scanstring 41 19 ./contrib/admin/views/main.py add_stage 51 19 ./core/validators.py RelaxNGCompact.__call__ 51 19 ./db/models/query.py lookup_inner 107 19 ./db/models/query.py delete_objects 48 19 ./template/__init__.py TokenParser.value 61 18 ./utils/simplejson/decoder.py JSONObject 37 18 ./utils/decorators.py decorator_from_middleware 25 18 ./contrib/comments/templatetags/comments.py DoGetCommentList.__call__ 31 18 ./contrib/comments/views/comments.py post_comment 66 18 ./utils/translation/trans_real.py get_language_from_request 40 17 ./core/mail.py send_mass_mail 31 17 ./views/generic/create_update.py create_object 38 16 ./views/static.py serve 28 16 ./core/servers/fastcgi.py runfastcgi 56 16 ./core/management.py get_sql_delete 49 16 ./db/models/query.py QuerySet.__getitem__ 38 16 ./utils/simplejson/encoder.py floatstr 17 15 ./utils/simplejson/encoder.py JSONEncoder._iterencode_dict 49 15 ./views/generic/create_update.py delete_object 36 15 ./contrib/comments/templatetags/comments.py DoCommentCount.__call__ 25 15 ./db/models/fields/related.py create_many_related_manager 73 15 ./db/models/query.py QuerySet._get_sql_clause 54 15 ./middleware/common.py CommonMiddleware.process_request 22 14 ./template/__init__.py Parser.parse 35 14 ./template/defaulttags.py do_if 33 14 ./template/defaulttags.py cycle 25 14 ./views/generic/date_based.py object_detail 43 14 ./contrib/comments/views/comments.py post_free_comment 41 14 ./contrib/admin/templatetags/admin_list.py items_for_result 59 14 ./contrib/admin/views/main.py get_javascript_imports 23 14 ./contrib/admin/views/main.py ChangeList.get_query_set 39 14 ./core/handlers/base.py BaseHandler.load_middleware 32 14 ./db/models/base.py Model.__init__ 33 14 ./template/defaulttags.py SsiNode.render 21 13 ./views/generic/list_detail.py object_list 46 13 ./contrib/auth/handlers/modpython.py authenhandler 28 13 ./db/models/fields/__init__.py Field.get_manipulator_fields 24 13 ./db/models/base.py ModelBase.__new__ 26 13 ./dispatch/saferef.py safeRef 65 12 ./dispatch/dispatcher.py _removeOldBackRefs 20 12 ./template/loader.py find_template_source 26 12 ./template/defaulttags.py IfNode.render 21 12 ./utils/timesince.py timesince 34 12 ./views/generic/date_based.py archive_month 36 12 ./core/management.py _get_sql_model_create 44 12 ./conf/__init__.py Settings.__init__ 26 12 ./db/models/fields/__init__.py DateTimeField.to_python 14 12 ./db/models/related.py RelatedObject.get_list 21 12 ./db/models/options.py Options.has_field_type 16 12 ./dispatch/dispatcher.py connect 34 11 ./template/defaultfilters.py pluralize 18 11 ./utils/simplejson/decoder.py JSONArray 21 11 ./views/generic/list_detail.py object_detail 33 11 ./views/generic/date_based.py archive_week 31 11 ./views/generic/date_based.py archive_day 34 11 ./contrib/redirects/middleware.py RedirectFallbackMiddleware.process_response 17 11 ./contrib/admin/templatetags/log.py DoGetAdminLog.__call__ 11 11 ./db/backends/postgresql/base.py DatabaseWrapper.cursor 21 11 ./db/backends/postgresql_psycopg2/base.py DatabaseWrapper.cursor 21 11 ./db/models/fields/related.py ReverseSingleRelatedObjectDescriptor.__get__ 19 11 ./db/models/query.py get_where_clause 18 11
Cyclomatic complexity for python code
We all know that maintanence is the biggest cost of software. You can keep the maintanence cost of your program down by writing readable code. More readable code means faster bug fixing, which leads to more time for adding new bugs.
One measure of readability is cyclomatic complexity, a software metric that measures a program’s complexity based on the number of distinct paths or branches in the code. Carnegie Mellon’s Software Engineering Institute defines the following complexity risk levels:
|
Cyclomatic Complexity |
Risk Evaluation |
|
1-10 |
a simple program, without much risk |
|
11-20 |
more complex, moderate risk |
|
21-50 |
complex, high risk program |
|
greater than 50 |
untestable program (very high risk) |
I have written a python script that take these complexity scripts a bit further by printing the most complex functions/methods in an entire python package specified on the command line. Save the following as complexity.py
#!/usr/bin/python
"""
Find the most complex functions/methods in your python code.
This program is offered freely into the public domain by
Gary Wilson (gary.wilson@gmail.com).
This code requires the complexity shell scripts found at
http://journyx.com/curt/complexity.html
Download the four files and then specify the path to the
"complexity" script below, as PATH_TO_COMPLEXITY.
Note: To get the scripts to work for me I had to change the line
open (T,"|$dn/tab|sort -n +5") || die "no tab?";
to
open (T,"|$dn/tab|sort -n -k 6") || die "no tab?";
This line appears once in "complexdef" and twice in "complexdefclass".
Example uses:
Search the current directory.
python complexity.py
Search the mypythonpackage package.
python complexity.py ~/mypythonpackage
You can also specify multiple files and/or directories.
python complexity.py ~/mypythonpackage /src/myscript.py
This script will search for all files ending in ".py" within the
paths specified on the command line. All functions and methods will
be sorted by decreasing complexity and printed along with the
filename and number of lines of code.
By default, this script will print out all functions/methods found.
If you would rather only see functions/methods with a certain
complexity or above, then change the COMPLEXITY_LEVEL setting below.
"""
## User Settings ###########################################
PATH_TO_COMPLEXITY = '~/complex/complexity'
# Only show functions/methods with a complexity greater than or
# equal to COMPLEXITY_LEVEL. Setting to 0 will print all functions/methods.
COMPLEXITY_LEVEL = 0
############################################################
import sys
import re
import os
from subprocess import Popen, PIPE
def complexity_for_file(filename):
cmd = '%s %s' % (PATH_TO_COMPLEXITY, filename)
p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=True)
errors = p.stderr.read()
if errors:
sys.exit(errors)
tuples = []
reached_functions = False
function_header = re.compile('Funcname|Classless_Function')
for line in p.stdout.readlines():
if function_header.match(line):
reached_functions = True
if reached_functions:
fields = line.split()
# We don't want the header lines.
try:
int(fields[5])
except:
continue
# filename, function name, lines of code, and complexity.
tuples.append((filename, fields[0], fields[4], fields[5]))
return tuples
def add_to_functions(functions, dir, filenames):
for filename in filenames:
if filename.endswith('.py'):
functions += complexity_for_file(os.path.join(dir, filename))
def get_column_widths(function_tuples):
widths = []
if not function_tuples:
return widths
for col in range(len(function_tuples[0])):
widths.append(max([len(ft[col]) for ft in function_tuples]))
return widths
def get_print_parms(widths, function_tuple):
parms = []
for x in range(len(widths)):
parms.append(widths[x])
parms.append(function_tuple[x])
return tuple(parms)
def main():
function_tuples = []
paths = sys.argv[1:] or os.path.curdir
for path in paths:
path = os.path.expanduser(path)
if os.path.isdir(path):
os.path.walk(path, add_to_functions, function_tuples)
elif os.path.isfile(path):
function_tuples += complexity_for_file(path)
# Filter out functions less than desired complexity level.
function_tuples = [t for t in function_tuples if int(t[-1]) >= COMPLEXITY_LEVEL]
# Sort by complexity.
function_tuples.sort(key=lambda t: int(t[-1]))
function_tuples.reverse()
print "\nShowing functions/methods with complexity greater than or equal to %s:\n" % COMPLEXITY_LEVEL
if function_tuples:
headers = ('Filename', 'Function/Method', 'Lines of Code', 'Complexity')
widths = get_column_widths(function_tuples + [headers])
print '%-*s %-*s %*s %*s' % get_print_parms(widths, headers)
for ft in function_tuples:
print '%-*s %-*s %*s %*s' % get_print_parms(widths, ft)
else:
print "None."
if __name__ == "__main__":
sys.exit(main())