Se supone que el operador % estará obsoleto en futuras versiones, aunque posiblemente podamos seguir usándolo algunos años. Personalmente no utilizo % cuando escribo código nuevo, aunque estoy de acuerdo que, para este ejemplo, la obligación de usar python2.6 solo por algo tan accesorio no merece la pena. Lo cambio.
Muy interesante y completo el artículo. Pero tengo algunas observaciones/dudas.
Como requisitos se exige python 2.2 mínimo, pero creo que es incorrecto, ya que se usan funciones como format() que solo aparecen a partir de 2.6. Luego en realidad sólo podría utilizarse a partir de 2.6.
No se tampoco la razón de usar format(), ya que su propósito es hacer sustituciones complejas, pero en esta porción de código:
raise TypeError("'{0}' given but '{1}' expected".format(
val.class.name, self.cls.name))
No se hace una sustitución muy elaborada, que podría ser reemplazada con:
raise TypeError("'%s' given but '%s' expected" %(
val.class.name, self.cls.name))
Y con esto posiblmente si sería compatible hasta Python 2.2
... es que yo creo que un mal diseño te conduce a esos "hacks" de manera directa.
El problema de ese alumno es un problema de diseño, entre otros. De hecho, así lo apuntas tú mismo al mencionar "rotaciones".
Una hoja y un boli suelen ser unos buenos elementos para programar. Otra cosa es que sólo vayas a picar código, esto es, "programming versus coding". Yo creo que codificar es parte de programar, y no al revés. Para programar hay que diseñar, para codificar no.
lo pondría aquí, pero son como 3000 líneas y el comentario iba a quedar algo largo 
Tras leer esto (sacado de aquí: http://msdn.microsoft.com/en-us/library/ms682629%28v=vs.85%29.aspx):
It is a good idea to use a large array, because it is hard to predict how many processes there will be at the time you call EnumProcesses.
To determine how many processes were enumerated, divide the pBytesReturned value by sizeof(DWORD). There is no indication given when the buffer is too small to store all process identifiers. Therefore, if pBytesReturned equals cb, consider retrying the call with a larger array.
...me he quedado así: 
Y ésta es la dirección del mejor compendio de antipatrones y mala programación que he visto: http://msdn.microsoft.com/en-us/library/dd162863%28v=VS.85%29.aspx
Odio con todas mis fuerzas tener que hacer algo en el innombrable...
Hombre, estás rediseñando la solución entera, cuando en ese pequeño snippet de código, lo que te apuñala los ojos es la semántica, la forma de expresar algo. Obviamente, soluciones (diseños) hay muchas, sólo me limité a poner la que era semántica y sintácticamente más parecida (de hecho, equivalente).
Me viene a la cabeza un caso que nos ha comentado David en alguna ocasión, y que sí que es un problema de diseño: un alumno, para resolver un juego de 3 en raya, anidó tropecientos if, de forma que en cada uno resolvía una posición particular del tablero. Y ni siquiera consideraba las posiciones equivalentes (por ejemplo, rotado 90, 180 y 270 grados).
... validate() no está de más, pero yo no dejaría al cliente utilizarlo. Sería un método privado. Si hay que comprobar en más de un sitio, validate() evitaría la redundancia.
My 2 cents,
si yo tuviera que resolver el problema, usaría una clase Tablero, que encapsularía una matriz de 8 x 8. Este objeto se encargaría de validar movimientos y demás. Supongo que en la matriz estaría compuesta de objetos que representasen si una casilla tiene una ficha o no, y cuál de ellas es. Sea como sea, las casillas no tendrían row/col, y si decidiera utilizar algún tipo de clase Casilla, usaría algo parecido al patrón "pesopluma", usando la información de row/col desde el tablero, no teniendo así la información de la posición por duplicado, y evitando posibles inconsistencias de datos. Nótese que el array/matriz donde colocásemos esas "coordenadas" ya tendría esa información.
... sí que los conocerías 
...you can use FUSE and this to do that... 
yep, I know it, but it is only a rsync front-end. The requirement here is transparent remote update.
Muy guapo lo de los anti-patrones, no lo conocía.
Mejorando aún más tu primer ejemplo ("Condición Múltiple"):
{ coords.validate(); // hacer cosas } //[...] class Coordinate { int row, col; public void validate() { if (row<1 || row>8 || col<'A' || col>'H') throw new InvalidCoordException(row, col); } }
Con eso has conseguido dos cosas: ya no tienes "row" y "col", sino "Coordinates", que es más orientado a objetos. Y te has cargado un comentario que no aportaba nada, mejorando la legibilidad del código.
Si me apuras, hasta la propia función validate es innecesaria, ya que al tener un objeto con las coordenadas puedes elevar la excepción en el momento en el que las coordenadas no sean válidas.
Finalmente, ahí va una lista de antipatrones: http://en.wikipedia.org/wiki/Anti-pattern
...no me había parado a pensar utilidades para dictfs.py... mi idea es hacer algo como zipfs, gitfs... alguna cosa de esas... porque dictfs.py lo pensé como ejemplo, nunca como utilidad...
Muy útil para hacer un mock del sistema de archivos y poder hacer pruebas de sistema con garantías de que la prueba es independiente.
Está bien tener esto a mano
Gracias.

Así da gusto.
Mi idea inicial era hacerlo en python pero llevo tanto tiempo diciendo que voy a aprender Python que ya ni me acuerdo. Y al final bash me resuelve siempre la papeleta de una u otra forma.
Lo bueno de que funcione en python es que no se dependería de ningún programa externo. Simplemente habría que utilizar los bindings de Gstreamer y lanzar desde ahí la reproducción, ya que Gstreamer utiliza modplug para reproducir los mod.
De todas formas, si os fijáis le he hecho unos cuantos cambios al código. Ahora ya no uso mikmod, he pasado a audacious, aunque cambiar a mikmod es relativamente simple, con poner
PLAYER=/usr/bin/mikmod
PLAYEROPTS="-i -hqmixer --surrond -X -noloops"
y eliminar la comprobación de que el player esté ejecutandose.
Ahora tarda mas al arrancar ya que recupera la búsqueda completa y de ahí crea una playlist, así nos evitamos que en una misma sesión se repitan pistas. Pero en cuanto se descarga el primer fichero ya se puede dar al play del audacious y empezar a escuchar, que el resto se van añadiendo a la cola conforme se descargan.
gracias
probe con <*code><*/code> pero no lo resaltaba, ¿cual era la etiqueta correcta?
... esto sí que es un buen background musical.
... editado para que se vea como código.
Bueno, mis dos céntimos, ya que al menos lo has liberado como software libre.
He portado el script bash de fsancho a python. Creo que requiere al menos python 2.7 ya que utiliza el módulo optparse.
Se puede descargar de:
http://www.shakaran.net/blog/wp-content/modarchive/modarchive.py
Os lo pongo al final de mi comentario también.
Ejemplo de ejecución:
python modarchive.py -s random -d
Resto de ejemplos en la ayuda (intentando mantener las opciones originales):
python modarchive.py -h
Supongo que es mejorable, no esta muy pythonico y tendrá algun bug (no he probado todas las acciones), pero es lo que he podido hacer en una horilla libre de la siesta.
Saludos
#!/bin/env python #*-* coding: utf-8 *-* """ ############################################################################### # MODARCHIVE JUKEBOX SCRIPT # # Made by: Fernando Sancho AKA ‘toptnc’ # email: toptnc@gmail.com # # This script plays mods from <a href="http://modarchive.org" title="http://modarchive.org">http://modarchive.org</a> in random order # It can fetch files from various categories # # This scripts needs the program mikmod installed under $PATH # # This script is released under the terms of GNU GPL License # ############################################################################### GPLv3 03-18-2011 Port to python by: Ángel Guzmán Maeso (shakaran at gmail dot com) <a href="http://shakaran.net/blog/wp-content/modarchive/modarchive.py " title="http://shakaran.net/blog/wp-content/modarchive/modarchive.py ">http://shakaran.net/blog/wp-content/modarchive/modarchive.py </a> """ from sys import exit from os import path, makedirs, mkdir, remove, system from urllib import urlopen import re import zipfile VERSION = '1.0' DEBUG = False MODPATH = '/tmp/modarchive' RANDOMSONG = None MIKMOD = '/usr/bin/mikmod' MODURL = None def get_mikmod_bin(): return MIKMOD def check_mikmod(): from os.path import exists as e return e(get_mikmod_bin()) if not check_mikmod(): print 'This scripts needs the binary MIKMOD to run. Please install it or change the script' exit(1) def get_url(url = None): try: return urlopen(url).read() except IOError, e: print _('Error: could not connect to the url' % url) return None def get_data(url = None, uregex = None): data = get_url(url) #print data data.replace('<>', '\n') if uregex: p = re.compile(r'(%s)' % uregex) m = p.search(data) if m: return m.group() else: print 'No se encontraron resultados' return None else: return data def get_mod_file(modurl, page): data = get_url('%s&page=%s' %(modurl, page)) data.replace('href=', '\n') data.replace('>', '\n') p = re.compile(r'(http://modarchive.org/data/downloads.php\?moduleid=(.*)\#)') m = p.search(data) if m: module = m.group()[50 : -150] module = module[0:- (len(module) - module.find('#')) ] debug(module) return ('http://modarchive.org/data/downloads.php?moduleid=%s' % module, module) else: print 'No se encontraron resultados' return (None, None) def unzip_file_into_dir(file, modID, dir): if not path.exists(dir + '/' + modID): mkdir(dir + '/' + modID, 0777) zfobj = zipfile.ZipFile(file) for name in zfobj.namelist(): if name.endswith('/'): if not path.exists(path.join(dir, name)): mkdir(path.join(dir, name)) else: outfile = open(path.join(dir + '/' + modID, name), 'wb') outfile.write(zfobj.read(name)) outfile.close() def debug(string = None): if DEBUG: print 'DEBUG:', string def usage(): return """ Modarchive Jukebox can be used with one of the following options: \n\n Hint: Use + symbol instead blankspaces in search strings.""" from argparse import ArgumentParser, RawTextHelpFormatter parser = ArgumentParser(prog='modarchive', formatter_class=RawTextHelpFormatter, add_help=True, description=usage(), version=VERSION) parser.add_argument('-d', '--debug', action='store_true', dest='debug', help='Enable debug mode') parser.add_argument('-s', dest='section', nargs=1, type=str, choices=('uploads', 'featured', 'favourites', 'downloads', 'topscore', 'new', 'random'), help="""Play from selected section: Can be one of this uploads This is a list of the recent member upload activity. featured These modules have been nominated by the crew for either \n\t outstanding quality, technique or creativity (or combination of). favourites These modules have been nominated by the members via their favourites. downloads The top 1000 most downloaded modules, recorded since circa 2002. topscore This chart lists the most revered modules on the archive. new Same than uploads but using search engine. random Ramdom module from entire archive.""") search_group = parser.add_argument_group('Searching') search_group.add_argument('-a', dest='artist', nargs=1, type=str, help='Search in artist database') search_group.add_argument('-m', dest='module', nargs=1, type=str, help="Search in module database (Title and Filename)") try: results = parser.parse_args() except IOError, msg: parser.error(str(msg)) if results.debug: DEBUG = True debug(results) pages = None if results.section: if results.section[0] == 'uploads': MODURL = 'http://modarchive.org/index.php?request=view_actions_uploads' pages = None elif results.section[0] == 'featured': MODURL = 'http://modarchive.org/index.php?request=view_chart&query=featured' pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.section[0] == 'favourites': MODURL = 'http://modarchive.org/index.php?request=view_top_favourites' pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.section[0] == 'downloads': MODURL = 'http://modarchive.org/index.php?request=view_chart&query=tophits' pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.section[0] == 'topscore': MODURL = 'http://modarchive.org/index.php?request=view_chart&query=topscore' pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.section[0] == 'new': MODURL = 'http://modarchive.org/index.php?request=search&search_type=new_additions' pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.section[0] == 'random': MODURL = 'http://modarchive.org/index.php?request=view_random' pages = 0 elif results.artist: MODURL = 'http://modarchive.org/index.php?query=%s&submit=Find&request=search&search_type=guessed_artist' % results.artist[0] pages = get_data(MODURL, 'page=(\w)#mods')[5:-5] elif results.module: MODURL = 'http://modarchive.org/index.php?request=search&query=%s&submit=Find&search_type=filename_or_songtitle' % results.module[0] res = get_data(MODURL, 'page=(\w)#mods')[5:-5] print "Starting Modarchive JukeBox Player" debug("Pages: %s" % pages) if not path.exists(MODPATH): debug('Creating dir %s' % MODPATH) makedirs(MODPATH) while 1: print 'Press Ctrl-C twice to stop' (modfile, modID) = get_mod_file(MODURL, pages) if modfile: zip_file = '%s/%s.zip' %(MODPATH, modID) print "Downloading %s to %s" %(modfile, zip_file) f = open(zip_file, 'w') f.write(get_url(modfile)) f.close() unzip_file_into_dir(zip_file, modID, MODPATH) remove(zip_file) # if [ ! -z $(pgrep pidgin) ]; # then # purple-remote "setstatus?status=available&message=Modarchive JukeBox: ${MODFILE}" # fi; print 'mikmod i -hqmixer --surrond X -noloops %s/%s/*' % (MODPATH, modID) print system('mikmod i -hqmixer --surrond X -noloops %s/%s/*' % (MODPATH, modID)) # if [ ! -z $(pgrep pidgin) ]; # then # purple-remote "setstatus?status=available&message=" # fi;
Corregido 
Gracias David.
Tu concepto de tontuna y el mío están muy alejados 