Metaclasses in Python, the easy way (a real life example)

They say that metaclasses make your head explode. They also say that if you're not absolutely sure what metaclasses are, then you don't need them.

And there you go, happily coding through life, jumping and singing in the meadow, until suddenly you get into a dark forest and find the most feared enemy: you realize that some magic needs to be done.

The necessity

Why you may need metaclasses? Let's see this specific case, my particular (real life) experience.

It happened that at work I have a script that verifies the remote scopes service for the Ubuntu Phone, checking that all is nice and crispy.

The test itself is simple, I won't put it here because it's not the point, but it's isolated in a method named _check, that receives the scope name and returns True if all is fine.

So, the first script version did (removed comments and docstrings, for brevity):

class SuperTestCase(unittest.TestCase):

    def test_all_scopes(self):
        for scope in self._all_scopes:
            resp = self._check(scope)
            self.assertTrue(resp)

The problem with this approach is that all the checks are inside the same test. If one check fails, the rest is not executed (because the test is interrupted there, and fails).

Here I found something very interesting, the (new in Python 3) subTest call:

class SuperTestCase(unittest.TestCase):

    def test_all_scopes(self):
        for scope in self._all_scopes:
            with self.subTest(scope=scope):
                resp = self._check(scope)
                self.assertTrue(resp)

Now, each "sub test" internally is executed independently of the other. So, they all are executed (all checks are done) no matter if one or more fail.

Awesome, right? Well, no.

Why not? Because even if internally everything is handled as independent subtest, from the outside point of view it still is one single test.

This has several consequences. One of those is that the all-inside test takes too long, and you can't know what was going on (note that each of these checks hit the network!), as the test runner just show progress per test (not subtest).

The other inconvenient is that there is not a way to call the script to run only one of those subtests... I can tell it to execute only the all-inside test, but that would mean to execute all the subtests... which, again, takes a lot of time.

So, what I really needed? Something that allows me to express the assertion in one test, but that in reality it were several methods. So, I needed something that, from a single method, reproduce them so the class actually had several ones. This is, write code for a class that Python would find different. This is, metaclasses.

Metaclasses, but easy

Luckily, since a couple of years ago (or more), Python provides a simpler way to achieve the same that could be done with metaclasses. This is: class decorators.

Class decorators, very similar to method decorators, receive the class that is defined below itself, and its response is considered by Python the real definition of the class. If you don't have the concept, you may read a little here about decorators, and a more deep article about decorators and metaclasses here, but it's not mandatory.

So, I wrote the following class decorator (explained below):

def test_multiplier(klass):
    """Multiply those multipliable tests."""
    for meth_name in (x for x in dir(klass) if x.startswith("test_")):
        meth = getattr(klass, meth_name)
        argspec = inspect.getfullargspec(meth)

        # only get those methods that are to be multiplied
        if len(argspec.args) == 2 and len(argspec.defaults) == 1:
            param_name = argspec.args[1]
            mult_values = argspec.defaults[0]

            # "move" the useful method to something not automatically executable
            delattr(klass, meth_name)
            new_meth_name = "_multiplied_" + meth_name
            assert not hasattr(klass, new_meth_name)
            setattr(klass, new_meth_name, meth)
            new_meth = getattr(klass, new_meth_name)

            # for each of the given values, create a new method which will call the given method
            # with only a value at the time
            for multv in mult_values:
                def f(self, multv=multv):
                    return new_meth(self, **{param_name: multv})

                meth_mult_name = meth_name + "_" + multv.replace(" ", "_")[:30]
                assert not hasattr(klass, meth_mult_name)
                setattr(klass, meth_mult_name, f)

    return klass

The basics are: it receives a class, it returns a slightly modified class ;). For each of the methods that starts with test_, I checked those that had two args (not only 'self'), and that the second argument were named.

So, it would actually get the method defined in the following structure and leave the rest alone:

@test_multiplier
class SuperTestCase(unittest.TestCase):

    def test_all_scopes(self, scope=_all_scopes):
        resp = self.checker.hit_search(scope, '')
        self.assertTrue(resp)

For that kind of method, the decorator will move it to something not named "test_*" (so we can call it but it won't be called by automatic test infrastructure), and then create, for each value in the "_scopes" there, a method (with a particular name which doesn't really matter, but needs to be different and is nice to be informative to the user) that calls the original method, passing "scope" with the particular value.

So, for example, let's say that _all_scopes is ['foo', 'bar']. Then, the decorator will rename test_all_scopes to _multiplied_test_all_scopes, and then create two new methods like this:

def test_all_scopes_foo(self, multv='foo'):
    return self._multiplied_test_all_scopes(scope=multv)

def test_all_scopes_bar(self, multv='bar'):
    return self._multiplied_test_all_scopes(scope=multv)

The final effect is that the test infrastructure (internally and externally) finds those two methods (not the original one), and calls them. Each one individually, informing progress individually, the user being able to execute them individually, etc.

So, at the end, all gain, no loss, and a fun little piece of Python code :)

Comentarios Imprimir

Novedades pythónicas: fades, CDPedia, Django y curso

Algunas, varias y sueltas.

A nivel de proyectos, le estuvimos metiendo bastante con Nico a fades. La verdad es que la versión 2 que sacamos la semana pasada está piolísima... si usás virtualenvs, no dejes de pegarle una mirada.

Otro proyecto con el que estuve es CDPedia... la parte de internacionalización está bastante potable, y eso también me llevó a renovar la página principal que te muestra cuando la abrís, así que puse a tirar una nueva versión de la de español, y luego seguirá una de portugués (¡cada imagen tarda como una semana!).

Hace un rato subí a la página de tutoriales de Python Argentina el Tutorial de Django en español (¡gracias Matías Bordese por el material!). Este tuto antes estaba en un dominio que ahora venció, y nos pareció interesante que esté todo en el mismo lugar, facilita que la gente lo encuentre.

Finalmente, empecé a organizar mi Segundo Curso Abierto de Python. Esta vez lo quiero hacer por la zona de Palermo, o alrededores (la vez pasada fue en microcentro). Todavía no tengo reservado un lugar, y menos fechas establecidas, pero el formato va a ser similar al anterior. Con respecto al sitio, si alguien conoce un buen lugar para alquilar "aulas", me avisa, :)

Comentarios Imprimir

Chau ACA

Unos meses atrás, justo cuando yo estaba de viaje en Washington, en el último sprint del laburo del año pasado, Moni tuvo problemas con el auto.

Un día que pasó a buscar a Felu por el jardín el auto no le arrancó. Pero lo importante no es el problema que tenía el auto, esta historia pasa por otro lado.

Moni llamó al Automóvil Club Argentino (que tengo desde hace más de diez años), para que la vengan a socorrer, y a priori no lo quisieron dar servicio. Le dijeron que ella no era la titular (lo cual es cierto, está a mi nombre), y que matanga. Ante la insistencia de Moni, le dijeron que lo iban a hacer sólo por esa oportunidad. Finalmente, la fueron a buscar, hubo un cambio de batería, etc, etc, final feliz.

Pero, ¿qué pasa si Moni vuelve a tener un problema con el auto?

Yo siempre creí que el ACA me cubría el auto, más allá de quien lo manejara. Parece que no. Según le dijeron a ella en ese momento, lo que luego confirmé en el call center, y luego personalmente en una sucursal, es que para que la cubran a ella se debería dar una de dos situaciones.

La primera, es que ella tenga una cédula azul del auto. Moni tiene la cédula verde del mismo (es tan dueña como yo), con lo cual no vamos a sacar la azul, y no tiene sentido que si la cédula es azul le den servicio, pero si es verde no.

La segunda es que haga una extensión familiar del servicio. Averigüé precios de esto, y es casi como sacar un segundo plan del ACA. Hoy por hoy la cuota del ACA es un poco alta, y sube un poquito todos los meses (todos los meses, eso me molesta bastante); duplicar ese costo no tiene sentido.

Bastante disgustado con toda esta situación, sopesé durante bastante tiempo la idea de darme de baja del servicio del Automóvil Club Argentino. Me cuesta un montón, porque me gustan un montón de cosas del ACA, su federalidad, la participación en el crecimiento de tantas ciudades pequeñas del país, etc... pero la verdad es que todo lo sucedido me rompió bastante las pelotas.

Mi viejo tuvo la idea de que exprese todo esto en una carta a la Comisión Directiva del club, a ver qué me decían. Armé un documento y se los presenté a fines de Noviembre. Me contestaron a mitad de Enero, un tal Juan Jorge Agüero ("Jefe Administrativo de Iniciativas y Observaciones de Socios"), en una carta toda escrita en mayúsculas en la que básicamente mandaba fruta.

¿Por qué fruta? Porque contestó un montón de generalidades, con cosas como (convertido a minúsculas por respeto a ustedes) "se procedió a realizar el traslado de su observación al área de auxilio mecánico a fin de que se tomen las medidas correctivas pertinentes..."; claro, no hay ninguna medida correctiva pertinente, así que no me sirve para nada.

En fin, tomé la decisión de irme del ACA.

Me voy a quedar con el seguro de La Caja, sí, que siempre me respondió en tiempo y forma. Tampoco es que voy a ahorrar guita, porque el seguro directo (con el precio del auto actualizado) es sólo un poco menos que el seguro más la cuota social del ACA sumados. Pero el gran diferencial es que el servicio mecánico que me dan ("AuxiCaja") me sirve más allá de quien esté manejando el auto.

Comentarios Imprimir

Cantando juntos

"Vení, cantemos algo a dúo."

Cantando a dúo

¡Rocanrol!

Comentarios Imprimir

Películas after verano, chabón

Estos meses le puse bastante pila a las películas... mitad que no vi tantas series (no arranqué ninguna nueva) y que tuve bastante tiempo solo en casa (me encanta aprovecharlo para ponerme tranquilo a ver una película, con la luz ambiente y el volumen que se me canta, sin tener que frenarla cada diez minutos por algo).

Ojo, también algunas de estas las vi con la familia, :). Y una en el cine (es obvio cual, ¿no?).

  • Cloud Atlas: +1. Una GRAN historia, contada de una forma bastante compleja. La peli es larga, y trabaja Tom Hanks, pero así y todo lo vale.
  • Elysium: +1. Me gustó bastante, la mezcla de futurismo bien hecho tanto en tecnología como en descripción de la sociedad siempre me atrae.
  • John Dies at the End: -1. De tan bizarra que aburre... tiene dos o tres flashes interesantes, pero no vale su peso en aire.
  • Movie 43: -0. Tiene algunos cortos con partes graciosas, la mayoría es una bizarrada. Si te gustan las pelis bizarras onda chachacha, dale una oportunidad. Lo que no entiendo es cómo pelis como esta juntan tantos actores de primera linea...
  • Now You See Me: +1. Muy bueno como se va armando el nudo y el desenlace... como en todo acto de magia, guarda con las distracciones...
  • Pearl Jam Twenty: +1. Muy buena película! No sólo la historia en sí de Pearl Jam, sino como está contada, mostrada, etc.
  • RED 2: +0. Muy violenta, como esperaba, pero también divertida (como también esperaba); pochoclera, está bien.
  • Red Lights: +1. Muy buenas actuaciones para una historia interesante. Cierra por todos lados.
  • Robot & Frank: +0. Una linda comedia o drama suave. Es interesante, no muy profunda, pero está bien.
  • Sin retorno: +0. Una buena historia, buenas actuaciones, te mantiene atrapado hasta el final.
  • The East: +1. Me gustó mucho, no sólo por la temática sino por cómo cuentan los sentimientos y actitudes de las personas involucradas.
  • The Hobbit: The Battle of the Five Armies: +0. Cierra bien, tiene puentes al resto de la historia, está bien; el detalle es que no es más que una historia para niños
  • The Man with the Iron Fists: -0. si te gustan las películas de orientales con karate exagerado, mirala, está muy bien hecha. Yo ya confirmo que nunca más una de estas.
  • The Numbers Station: +0. Una buena película de acción y suspenso.
  • The Wolverine: -0. Es como todo un poco más de lo mismo... la historia no está del todo mal, pero bleh.
  • Upside Down: +0. Una comedia romántica ligera, pero muy interesante a nivel de ciencia ficción. Un poco apurado todo al final, sino merecía más puntaje...
  • World War Z: +0. Bastante buena para ser una película de zombies...
Escena de Elysium

No hay tantas nuevas anotadas. Había más, realmente, producto de ir viendo trailers, pero al preparar este post me di cuenta que cuatro ya las había anotado antes :)

  • Avengers: Age of Ultron (2015; Action, Adventure, Fantasy, Sci-Fi) When Tony Stark tries to jumpstart a dormant peacekeeping program, things go awry and Earth's Mightiest Heroes, including Iron Man, Captain America, Thor, The Incredible Hulk, Black Widow and Hawkeye, are put to the ultimate test as the fate of the planet hangs in the balance. As the villainous Ultron emerges, it is up to The Avengers to stop him from enacting his terrible plans, and soon uneasy alliances and unexpected action pave the way for a global adventure. [D: Joss Whedon; A: Scarlett Johansson, Hayley Atwell, Chris Evans]
  • Clouds of Sils Maria (2014; Drama) At the peak of her international career, Maria Enders is asked to perform in a revival of the play that made her famous twenty years ago. But back then she played the role of Sigrid, an alluring young girl who disarms and eventually drives her boss Helena to suicide. Now she is being asked to step into the other role, that of the older Helena. She departs with her assistant to rehearse in Sils Maria; a remote region of the Alps. A young Hollywood starlet with a penchant for scandal is to take on the role of Sigrid, and maria finds herself on the other side of the mirror, face to face with an ambiguously charming woman who is, in essence, an unsettling reflection of herself. [D: Olivier Assayas; A: Juliette Binoche, Kristen Stewart, Chloë; Grace Moretz]
  • Fantastic Four (2015; Action, Fantasy, Sci-Fi) FANTASTIC FOUR, a contemporary re-imagining of Marvel's original and longest-running superhero team, centers on four young outsiders who teleport to an alternate and dangerous universe, which alters their physical form in shocking ways. Their lives irrevocably upended, the team must learn to harness their daunting new abilities and work together to save Earth from a former friend turned enemy. [D: Josh Trank; A: Kate Mara, Miles Teller, Toby Kebbell]
  • Home Sweet Hell (2015; Comedy, Drama) Don Champagne seems to have it all: a successful business, a perfect house, perfect kids and a perfect wife. Unfortunately, when his wife, Mona (Katherine Heigl), learns of Don's affair with a pretty new salesgirl (Jordana Brewster), this suburban slice of heaven spirals out of control. Don soon realizes that Mona will stop at nothing, including murder, to maintain their storybook life where "perception is everything". [D: Anthony Burns; A: Katherine Heigl, Jordana Brewster, Patrick Wilson]
  • Match (2014; Comedy, Drama, Music) Tobi Powell (Patrick Stewart), an aging Juilliard dance professor with a colorful and international past, is interviewed by a woman and her husband (Carla Gugino & Matthew Lillard) for a dissertation she's writing about the history of dance in New York in the 1960's. As the interview proceeds, it becomes increasingly clear that there are ulterior motives to the couple's visit. Explosive revelation is followed by questions about truth versus belief. MATCH is a story about responsibility, artistic commitment...and love. [D: Stephen Belber; A: Patrick Stewart, Carla Gugino, Matthew Lillard]
  • VANish (2015; Action, Crime, Horror, Thriller) A kidnapped young woman is forced on a road trip full of murder and mayhem that takes place entirely in her captor's getaway van. [D: Bryan Bockbrader; A: Maiara Walsh, Tony Todd, Danny Trejo]
  • Vice (2015; Action, Adventure, Sci-Fi, Thriller) Julian Michaels (Bruce Willis) has designed the ultimate resort: VICE, where anything goes and the customers can play out their wildest fantasies with artificial inhabitants who look, think and feel like humans. When an artificial (Ambyr Childers) becomes self-aware and escapes, she finds herself caught in the crossfire between Julian's mercenaries and a cop (Thomas Jane) who is hell-bent on shutting down Vice, and stopping the violence once and for all. [D: Brian A Miller; A: Ambyr Childers, Thomas Jane, Bryan Greenberg]
  • Danny Collins (2015; Comedy, Drama) Inspired by a true story, Al Pacino stars as aging 1970s rocker Danny Collins, who can't give up his hard-living ways. But when his manager (Christopher Plummer) uncovers a 40 year-old undelivered letter written to him by John Lennon, he decides to change course and embarks on a heartfelt journey to rediscover his family, find true love and begin a second act. [D: Dan Fogelman; A: Melissa Benoist, Al Pacino, Jennifer Garner]
  • Ex Machina (2015; Drama, Sci-Fi) Caleb, a 26 year old coder at the world's largest internet company, wins a competition to spend a week at a private mountain retreat belonging to Nathan, the reclusive CEO of the company. But when Caleb arrives at the remote location he finds that he will have to participate in a strange and fascinating experiment in which he must interact with the world's first true artificial intelligence, housed in the body of a beautiful robot girl. [D: Alex Garland; A: Oscar Isaac, Alicia Vikander, Domhnall Gleeson]
  • Into the Woods (2014; Adventure, Comedy, Fantasy, Musical) Into the Woods is a modern twist on the beloved Brothers Grimm fairy tales in a musical format that follows the classic tales of Cinderella, Little Red Riding Hood, Jack and the Beanstalk, and Rapunzel-all tied together by an original story involving a baker and his wife, their wish to begin a family and their interaction with the witch who has put a curse on them. [D: Rob Marshall; A: Anna Kendrick, Daniel Huttlestone, James Corden]
  • Pan (2015; Adventure, Comedy, Family, Fantasy) The story of an orphan who is spirited away to the magical Neverland. There, he finds both fun and dangers, and ultimately discovers his destiny -- to become the hero who will be forever known as Peter Pan. [D: Joe Wright; A: Amanda Seyfried, Rooney Mara, Hugh Jackman]
  • Predestination (2014; Action, Drama, Mystery, Sci-Fi, Thriller) PREDESTINATION chronicles the life of a Temporal Agent sent on an intricate series of time-travel journeys designed to ensure the continuation of his law enforcement career for all eternity. Now, on his final assignment, the Agent must pursue the one criminal that has eluded him throughout time. [D: Michael Spierig, Peter Spierig; A: Ethan Hawke, Sarah Snook, Christopher Kirby]
  • Terminator Genisys (2015; Action, Adventure, Sci-Fi, Thriller) After finding himself in a new time-line, Kyle Reese teams up with John Connor's mother Sarah and an aging terminator to try and stop the one thing that the future fears, "Judgement Day". [D: Alan Taylor; A: Emilia Clarke, J.K. Simmons, Arnold Schwarzenegger]
Escena del trailer de Ex Machina

Finalmente, el conteo de pendientes por fecha:

(Ene-2009)    1
(May-2009)
(Oct-2009)
(Mar-2010)   16   4
(Sep-2010)   18  18   9   2   1
(Dic-2010)   12  12  12   5   1
(Abr-2011)   23  23  23  22  17   4
(Ago-2011)   11  11  11  11  11  11   4
(Ene-2012)   21  18  17  17  17  17  11   3
(Jul-2012)   15  15  15  15  15  15  14  11
(Nov-2012)       12  12  11  11  11  11  11   6
(Feb-2013)           19  19  16  15  14  14   8   2
(Jun-2013)               19  18  16  15  15  15  11
(Sep-2013)                   18  18  18  18  17  16
(Dic-2013)                       14  14  12  12  12
(Abr-2014)                            9   9   8   8
(Jul-2014)                               10  10  10
(Nov-2014)                                   24  22
(Feb-2015)                                       13
Total:      117 113 118 121 125 121 110 103 100  94
Comentarios Imprimir