As what as you like

  • Python decisions

    Every time I find myself hacking on some Python I find myself second guessing all sorts of tiny design decisions and so I figure the only way to get any kind of perspective on them is to talk about them. Either I’ll achieve more clarity through constructing explanations of what I was thinking, or people will comment with useful insights. Hopefully the latter, but this is hardly the most popular blog in the world ;) So. What shall we look at first. Well, I just hacked up a tiny script last night to answer a simple question:

    Is most of my music collection from the 90s?

    Obviously what I want to do here is examine the ID3 tags of the files in my music collection and see how they’re distributed. A quick search with apt showed that Ubuntu 10.04 has two python libraries for dealing with ID3 tags and a quick play with each suggested that the one with the API most relevant to my interests was eyeD3. After a few test iterations of the script I was getting bored of waiting for it to silently scan the roughly 4000 MP3s I have, so I did another quick search and found a progress barlibrary. So that’s all of the motive and opportunity established, now let’s examine the means to the end. If you want to follow along at home, the whole script is here.

    try:
      import eyeD3
      import progressbar as pbar
    except ImportError:
      print("You should make sure python-eyed3 and python-progressbar are installed")
     sys.exit(1)
    

    First off this is the section where I’m importing the two non-default python libraries that I depend on. I want to provide a good experience when they’re not installed, so I catch the exception and tell people the Debian/Ubuntu package names they need, and exit gracefully. I rename the progressbar module as I import it just because “progressbar” is annoyingly long as a name, and I don’t like doing “from foo import *”. Skipping further on, we find the code that extracts the ID3 year tag:

    year = tag.getYear() or 'Unknown'
    

    This is something I’m really not sure about the “correctness” of; One of the reasons I went with the eyeD3 library was that the getYear() method returns None if it can’t find any data, but I don’t really want to capture the result, then test the result explicitly and if it’s None set the value to “Unknown”, so I went with the above code which only needs a single line and is (IMHO) highly readable. This is ultimately the crux of the entire program - we’ve now collected the year, so we can work out which decade it’s from:

    if year is not 'Unknown':
     year = "%s0s" % str(year)[:3]
    

    If this isn’t an unknown year we chop the final digit off the year and replace it with a zero. Job done! Next up, another style question. Rather than store the year we just processed I want to know how many of each decade have been found, so the obvious choice is a dict where the keys are the decades and the values are the number of times each decade has been found. One option would be to pre-fill the dict with all the decades, each with a value of zero, but that seems redundant and ugly, so instead I start out with an empty dict. This presents a challenge - if we find a decade that isn’t already a key in the dict (which will frequently be the case) we need to notice that and add it. We could do this by pre-emptively testing the dict with its has_key() method, but that struck me as annoyingly wordy, so I went with:

    try:
      years[year] += 1
    except KeyError:
      years[year] = 1
    

    If we are incrementing a year that isn’t already in the dict, python will raise a KeyError, at which point we know what’s happened and know the correct value is 1, so we just set it explicitly. Seems like the simplest solution, but is it the sanest? The only other thing I wanted to say is a complaint - having built up the dict I then want to print it nicely, so I have a quick list comprehension to produce a list of strings of the format “19xx: yy” (i.e. the decade and the final number of tracks found for that decade), which I then join together using:

    ', '.join(OUTPUT)
    

    which I hate! Why can’t I do:

    OUTPUT.join(', ')
    

    (where “OUTPUT” is the list of strings). If that were possible, what I’d actually do is tack the .join() onto the end of the list comprehension and a single line would turn the dict into a printable string. So there we have it, my thoughts on the structure of my script. I’d also add that I’ve become mildly obsessive about getting good scores from pylint on my code, which is why it’s rigorously formatted, docstring-ed and why the variable names in the __main__ section are in capitals. What are your thoughts? Oh, and the answer is no, most of my music is from the 2000s. The 1990s come in second :)

  • gtk icon cache search tool

    Earlier on this evening I was asking the very excellent Ted Gould about a weird problem with my Gtk+ icon theme - an app I’d previously installed by hand in /usr/local/, but subsequently removed, had broken icons because Gtk+ was looking in /usr/local/share/icons/ instead of /usr/share/icons/. We did a little digging and realised I had an icon theme cache file in /usr/local/ that was overriding the one in /usr/. A bit of deleting later and it’s back, but in the process we whipped up a little bit of python to print out the filename of an icon given an icon name.

    #!/usr/bin/python
    # gtk-find-icon by Chris Jones <cmsj@tenshu.net>
    # Copyright 2010. GPL v2.
    
    import sys
    import gtk
    
    THEME = gtk.IconTheme()
    ICON = THEME.lookup_icon(sys.argv[1],
     gtk.ICON_SIZE_MENU,
     gtk.ICON_LOOKUP_USE_BUILTIN)
    
    if not ICON:
     print "None found"
    else:
     print(ICON.get_filename())
    
  • Hybrid - Disappear Here

    It’s a while since I wrote anything in the Music category of this blog, and since everything has been about my software projects recently, I figure it’s time to mix things up a little. It’s also just a few weeks since the release of the latest studio album by probably my favourite band of the last few years, Hybrid. The album is called Disappear Here and it’s pretty damn good - I was going to describe the tracks individually, but that’s what reviewers typically do and it always sounds insufferably poncy, so I suggest you just go to the album’s site and listen to the damn thing yourself ;) They also post semi-frequent hour long DJ mixes on their Soundcloud page, which I would recommend!

  • Writing Terminator plugins

    Terminator Plugin HOWTO

    One of the features of the new 0.9x series of Terminator releases that hasn’t had a huge amount of announcement/discussion yet is the plugin system. I’ve posted previously about the decisions that went into the design of the plugin framework, but I figured now would be a good time to look at how to actually take advantage of it. While the plugin system is really generic, so far there are only two points in the Terminator code that actually look for plugins - the Terminal context menu and the default URL opening code. If you find you’d like to write a plugin that interacts with a different part of Terminator, please let me know, I’d love to see some clever uses of plugins and I definitely want to expand the number of points that plugins can hook into. The basics of a plugin ———————-

    A plugin is a class in a .py file in terminatorlib/plugins or ~/.config/terminator/plugins, but not all classes are automatically treated as plugins. Terminator will examine each of the .py files it finds for a list called ‘available’ and it will load each of the classes mentioned therein. Additionally, it would be a good idea to import terminatorlib.plugin as that contains the base classes that other plugins should be derived from. A quick example:

    import terminatorlib.plugin as plugin
    available = ['myfirstplugin']
    class myfirstplugin(plugin.SomeBasePluginClass):
      etc.
    

    So now let’s move on to the simplest type of plugin currently available in Terminator, a URL handler. URL Handlers ————

    This type of plugin adds new regular expressions to match text in the terminal that should be handled as URLs. We ship an example of this with Terminator, it’s a handler that adds support for the commonly used format for Launchpad. Ignoring the comments and the basics above, this is ultimately all it is:

    class LaunchpadBugURLHandler(plugin.URLHandler):
      capabilities = ['url_handler']
      handler_name = 'launchpad_bug'
      match = '\\b(lp|LP):?\s?#?[0-9]+(,\s*#?[0-9]+)*\\b'
    
      def callback(self, url):
        for item in re.findall(r'[0-9]+', url):
          return('https://bugs.launchpad.net/bugs/%s' % item)
    

    That’s it! Let’s break it down a little to see the important things here:

    • inherit from plugin.URLHandler if you want to handle URLs.
    • include ‘url_handler’ in your capabilities list
    • URL handlers must specify a unique handler_name (no enforcement of uniqueness is performed by Terminator, so use some common sense with the namespace)
    • Terminator will call a method in your class called callback() and pass it the text that was matched. You must return a valid URL which will probably be based on this text.

    and that’s all there is to it really. Next time you start terminator you should find the pattern you added gets handled as a URL! Context menu items ——————

    This type of plugin is a little more involved, but not a huge amount and as with URLHandler we ship an example in terminatorlib/plugins/custom_commands.py which is a plugin that allows users to add custom commands to be sent to the terminal when selected. This also brings a second aspect of making more complex plugins - storing configuration. Terminator’s shiny new configuration system (based on the excellent ConfigObj) exposes some API for plugins to use for loading and storing their configuration. The nuts and bolts here are:

    import terminatorlib.plugin as plugin
    from terminatorlib.config import Config
    available = ['CustomCommandsMenu']
    class CustomCommandsMenu(plugin.MenuItem):
      capabilities = ['terminal_menu']
      config = None
      def __init__(self):
        self.config = Config()
        myconfig = self.config.plugin_get_config(self.__class__.__name__)
        # Now extract valid data from sections{}
      def callback(self, menuitems, menu, terminal):
        menuitems.append(gtk.MenuItem('some jazz'))
    

    This is a pretty simplified example, but it’s sufficient to insert a menu item that says “some jazz”. I’m not going to go into the detail of hooking up a handler to the ‘activate’ event of the MenuItem or other PyGTK mechanics, but this gives you the basic detail. The method that Terminator will call from your class is again “callback()” and you get passed a list you should add your menu structure to, along with references to the main menu object and the related Terminal. As the plugin system expands and matures I’d like to be more formal about the API that plugins should expect to be able to rely on, rather than having them poke around inside classes like Config and Terminal. Suggestions are welcome :) Regarding the configuration storage API - the value returned by Config.plugin_get_config() is just a dict, it’s whatever is currently configured for your plugin’s name in the Terminator config file. There’s no validation of this data, so you should pay attention to it containing valid data. You can then set whatever you want in this dict and pass it to Config().plugin_set_config() with the name of your class and then call Config().save() to flush this out to disk (I recommend that you be quite liberal about calling save()). Wrap up ——-

    Right now that’s all there is to it. Please get in touch if you have any suggestions or questions - I’d love to ship more plugins with Terminator itself, and I can think of some great ideas. Probably the most useful thing would be something to help customise Terminator for heavy ssh users (see the earlier fork of Terminator called ‘ssherminator’)

  • Terminator 0.93 released!

    Another week, another release focused on squashing as many bugs as possible. There’s also one feature in this release - a patch from Kees Cook to add a preferences UI for the alternate_screen_scroll setting. Please keep those bug reports coming, the response to the 0.9x series has been fantastic!

Complete post archive