Tue 30 January 2018

Unerwartet: def vs. val

Nach einem Jahr der Blog-Abstinenz heute zur Erfrischung mal ein wenig Scala. Nehmen wir an, wir haben die folgende login-Funktion, die REST-assured nutzt, um HTTP-Requests für einen Login zu tätigen. Zurückgegeben wird ein Tupel mit einem Access-Token und wie lange selbiges gültig ist.

def login(credentials: Credentials): (String, Int) = {
  def json = given()
    // Skipped: add the credentials somehow to the request
    .post("…")
    .Then
    .statusCode(HttpStatus.SC_OK)
    .extract()
    .jsonPath()

  (json.getString("access_token"), json.getInt("expires_in"))
}

Manchmal gibt die Methode ein Access-Token zurück, das nicht gültig ist, obwohl es laut der ebenfalls zurückgegebenen Gültigkeitsdauer noch lange gültig sein sollte. Warum?

Sehr subtil, aber das def json = … sollte natürlich ein val json = … sein. Ansonsten findet bei der Erstellung des Rückgabe-Tupel zwei mal ein Aufruf der Funktion json statt: einmal bei der Auswertung von json.getString("access_token") und das andere Mal bei der Auswertung von json.getInt("expires_in"). Beides mal wird natürlich auch ein frischer HTTP-Request abgesetzt. Es konnte daher passieren, dass für das Access-Token ein Token zurückgegeben wurde, das gerade am ablaufen ist, und für den expires_in-Wert wurde bereits ein neues Token ausgestellt. Daher sieht es so aus, als wäre das Token noch lange gültig, dabei ist es bereits abgelaufen.

Thu 03 December 2015

Let's Encrypt!

Seit heute steht Let's Encrypt als öffentliche Beta zur Verfügung. Mit Let's Encrypt kann sich jeder einfach und kostenlos ein X.509-Zertifikat erstellen lassen. Somit gibt es keine Ausrede mehr, nicht HTTPS zu benutzen. Deswegen versucht dieses Blog auch mit gutem Beispiel voran zu gehen:

Sun 24 August 2014

Generic Heterogenous Containers Using Super Type Tokens

Wie man am Titel bereits leicht erraten kann: heute geht es (zum ersten Mal in diesem Blog?) um Java. Genauer: Wie man heterogene Container in Java umsetzen kann.

Zunächst zur Idee: Wir wollen eine Klasse Favorites implementieren, in die man seine Lieblingsobjekte speichern und auch wieder abrufen kann. Damit man nicht einfach irgendein beliebiges Objekt zurückbekommt, sondern ein Objekt von einem bestimmten Typ, übergibt man beim Speichern und abrufen die Klasse des Objekts.

Die API dafür sieht so aus:

public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> getFavorite(Class<T> type);
}

Note

Der geneigte Java-erfahrene Leser wird direkt erkennen, dass das Beispiel aus Effective Java von Joshua Bloch ist. Das ist absolut richtig und auch Teile des hier gezeigten Codes stammen aus diesem Buch, das durchaus lesenswert ist (auch für Nicht-Java-Programmierer).

Die erste Idee, das zu implementieren, könnte etwa so aussehen:

public class Favorites {
    private final Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
        favorites.put(checkNotNull(type), instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

Man nimmt also einfach das Klassenobjekt des Werts als Schlüssel in einer Map, um den Wert zu speichern. Beim Auslesen wird dann auch wieder das Klassenobjekt übergeben, womit man an den Wert kommt. Wurde für den Typ kein Wert hinterlegt, wird einfach nichts gefunden.

An sich scheint das auch ganz gut zu funktionieren:

Favorites f = new Favorites();
f.putFavorite(String.class, "Some string");
f.putFavorite(Integer.class, 1234);
System.out.printf("%s %d%n", f.getFavorite(String.class), f.getFavorite(Integer.class));

Auch der Fall, dass kein Wert hinterlegt wurde für den Typ, funktioniert einfach: favorites.get(type) gibt dann null zurück und null kann zu allem gecastet werden.

Und hätte Java jetzt nicht Type Erasure, wäre der Blogpost auch schon zu Ende. Da Java allerdings Type Erasure hat, stößt man recht bald auf ein Problem, wenn man auf versucht, ein Generic in den Container zu packen: List<Integer>.class ist ein Syntaxfehler. Das liegt daran, dass List<Integer> und List<String> dasselbe Klassenobjekt haben, nämlich List.class. Das bedeutet aber auch, dass man eine Liste von Strings in Favorites packen kann und danach als Liste von Integern auslesen kann (bzw. man kann es zumindest versuchen):

f.putFavorite(List.class, Arrays.asList("foo", "bar"));
List<Integer> values = f.getFavorite(List.class);

Wenn man das jetzt ausführt, scheint es sogar zu funktionieren. Jedenfalls läuft es anstandslos durch. Allerdings nur bis man dann den Wert tatsächlich einmal versucht als den angegebenen Wert zu verwenden:

Integer first = values.get(0);

Bei einer erneuten Ausführung wird jetzt eine Ausnahme geworfen:

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

Bei genauem Hinschauen erkennt man auch bereits beim Kompilieren, dass hier potenziell zur Laufzeit etwas kaputt gehen könnte:

Favorites.java: warning: [unchecked] unchecked conversion
      List<Integer> values = f.getFavorite(List.class);
                                          ^
required: List<Integer>
found:    List

Ein gutes Beispiel dafür, dass man Compiler-Warnungen nicht einfach ignorieren sollte.

Das ist natürlich wenig zufriedenstellend. Ein Ausweg daraus sind die sogenannten Super Type Tokens, die man wie folgt verwenden kann:

TypeReference<List<String>> x = new TypeReference<List<String>>() {};

TypeReference kann jetzt zur Laufzeit nachsehen, welcher Wert als Typvariable übergeben wurde. Das wiederum kann dann Favorite für sich benutzen.

Die Implementierung von TypeReference:

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public abstract class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class<?>) {
            throw new RuntimeException("Missing type parameter");
        }

        type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }

    public Type getType() {
        return type;
    }
}

Und das angepasste Favorites:

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Favorites {
    private final Map<Type, Object> favorites = new HashMap<>();

    public <T> void putFavorite(TypeReference<T> typeReference, T instance) {
        favorites.put(typeReference.getType(), instance);
    }

    @SuppressWarnings("unchecked")
    public <T> T getFavorite(TypeReference<T> typeReference) {
        return (T) favorites.get(typeReference.getType());
    }

    public static void main(String[] args) {
        Favorites f = new Favorites();
        f.putFavorite(new TypeReference<String>() {}, "Some string");
        f.putFavorite(new TypeReference<Integer>() {}, 1234);
        f.putFavorite(new TypeReference<List<String>>() {}, Arrays.asList("foo", "bar"));
        System.out.printf("%s %d %s%n",
                          f.getFavorite(new TypeReference<String>() {}),
                          f.getFavorite(new TypeReference<Integer>() {}),
                          f.getFavorite(new TypeReference<List<Integer>>() {}));
    }
}

Die Ausgabe ist wie erwartet Some string 1234 null. Fertig ist er also, unser typsicherer heterogener Container, der auch mit Generics funktioniert (wenn auch etwas umständlich).

Oder fast. Wenn da dieses @SuppressWarnings("unchecked") nicht wäre. Immerhin haben wir vor einem Augenblick erst gesehen, dass man Compiler-Warnungen nicht ignorieren sollte. Und tatsächlich kann man auch für die neue Favorites einen Fall konstruieren, bei dem zur Laufzeit eine Ausnahme fliegt:

static <T> List<T> favoriteList(Favorites f) {
    TypeReference<List<T>> typeReference = new TypeReference<List<T>>(){};
    List<T> result = f.getFavorite(typeReference);
    if (result == null) {
        result = new ArrayList<T>();
        f.putFavorite(typeReference, result);
    }
    return result;
}

public static void main(String[] args) {
    Favorites f = new Favorites();
    List<String> listOfStrings = favoriteList(f);
    List<Integer> listOfIntegers = favoriteList(f);
    listOfIntegers.add(42);
    String booooom = listOfStrings.get(0);
}

// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

Um diese Ausnahme zu vermeiden, müsste man die Typ-Argumente durchgehen und schauen, ob noch irgendwo eine TypeVariable vorkommt. Ein Beispiel, in dem das gemacht wird, ist GenericType<T> aus JAX-RS (Quelltext). Das würde aber noch immer zur Laufzeit eine Exception werfen und nicht zur Compile-Zeit einen Fehler produzieren.

Wed 20 August 2014

Neues bpaste.net: Pinnwand

ikanobori hat sich die Mühe gemacht und die Software, die auf bpaste.net läuft, komplett erneuert. Dabei hat er auch lange gewünschte neue Features umgesetzt. Außerdem ist bpaste.net damit gleichzeitig auf neue, leistungsfähigere Hardware umgezogen.

Neu ist:

  • Man kann Pastes löschen (via einem Löschen-Link)
  • Pastes können sich automatisch nach einer bestimmten Zeit löschen

Gleich geblieben ist:

  • API-Kompatibilität zu LodgeIt (hoffentlich zumindest)

Die Daten werden innerhalb der nächsten zwei Monate automatisch in das neue System übertragen. Aber nur, wenn man ein altes Paste öffnet. Danach werden die Daten unwiderruflich gelöscht sein.

Der Code befindet sich auf Github. Wünsche, Bugs und Anregungen bitte melden.

Wed 20 August 2014

Emacs: Open file in browser

Inspiriert vom Idea-GitLab-Integration-Plugin, das ein open file in browser anbietet, habe ich das ganze mal für Emacs gebaut:

(require 'magit)

(setq git-browser-url-templates
      '(("bitbucket.org" . "https://bitbucket.org/{{repo}}/src/{{branch}}/{{file-name}}#cl-{{lineno}}")
     ("github.com" . "https://github.com/{{repo}}/blob/{{branch}}/{{file-name}}#L{{lineno}}")
     ("pwmt.org" . "https://git.pwmt.org/?p={{repo}}.git;a=blob;f={{file-name}};hb={{branch}}#l{{lineno}}")))

(defun format-gitviewer-url (template vars)
  (let ((expand (lambda (match)
               (let* ((name (substring match 2 -2))
                      (value (assoc name vars)))
                 (unless value
                   (error (format "Unknown variable %s" name)))
                 (cdr value)))))
    (replace-regexp-in-string "{{.+?}}" expand template)))


(defun git-open-in-browser ()
  (interactive)
  (let* ((remote (magit-get-remote nil))
      (remote-url (magit-get "remote" remote "url"))
      (branch (substring (magit-get-tracked-branch) (+ 1 (length remote))))
      (file-name (magit-file-relative-name (buffer-file-name)))
      (lineno (line-number-at-pos)))
    (unless (string-match "\\(@\\|://\\|^\\)\\([^:@/]+?\\)[:/]\\([^/].*?\\)\\(.git\\)?$" remote-url)
      (error "Could not find repo name"))
    (let* ((host-name (match-string 2 remote-url))
        (repo-name (match-string 3 remote-url))
        (url-template (assoc host-name git-browser-url-templates)))
      (unless url-template
     (error (format "Could not find URL template for host %s" host-name)))
      (browse-url (format-gitviewer-url (cdr url-template)
                                     (list
                                      (cons "repo" repo-name)
                                      (cons "branch" branch)
                                      (cons "file-name" file-name)
                                      (cons "lineno" (number-to-string lineno))))))))

Wed 20 August 2014

Dekoratoren in Python, erklärt in Farbe

Präambel

Dieser Artikel ist schon ein paar Jahre alt und war eigentlich nur für den Gebrauch bei meinem damaligen Arbeitgeber gedacht. Damit sich aber endlich mal etwas in diesem Blog tut, habe ich mich entschlossen, ihn dennoch zu veröffentlichen.

Note

Die Code-Beispiele sind in Python 2-Syntax gehalten. Das Übersetzen nach Python 3 wird dem Leser als Übung überlassen.

Was sind Dekoratoren?

Wie der Namen bereits vermuten lässt, dekorieren Dekoratoren etwas. Bei Python sind es Funktionen bzw. Methoden und seit Version 2.6/3.0 auch Klassen. Ein Beispiel für Dekoratoren:

@decorator
def spam():
    print "Ich bin spam."

Note

Ein Hinweis für Java-Entwickler: Die Syntax sieht zwar ähnlich aus wie Annotations in Java, hat aber ansonsten nichts mit Annotations in Java zu tun.

Was passiert jetzt, wenn man den obigen Code ausführt?

>>> @decorator
... def spam():
...     print "Ich bin spam."
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'decorator' is not defined

Der NameError ist jetzt natürlich wenig überraschend, schließlich wurde der Dekorator decorator noch nicht definiert. Im Folgenden werden wir das nachholen:

def decorator(func):
    print "Ich bin ein Dekorator und dekoriere", func.__name__
    return func

Was passiert jetzt, wenn man den obigen Code ausführt?

>>> def decorator(func):
...     print "Ich bin ein Dekorator und dekoriere", func.__name__
...     return func
...
>>> @decorator
... def spam():
...     print "Ich bin spam."
...
Ich bin ein Dekorator und dekoriere spam
>>> spam
<function spam at 0x7f8d95b3c488>

Und was, wenn man die dekorierte Funktion ausführt?

>>> spam()
Ich bin spam.

Wie einfach zu erkennen ist, hat unser simpler Beispiel-Dekorator keine direkte Auswirkung auf die dekorierte Funktion.

Ich weiß jetzt noch immer nicht, was ein Dekorator ist

Ein Beispiel alleine erklärt natürlich nicht, was ein Dekorator ist. Was ist also ein Dekorator? Ein Dekorator ist nichts anderes als ein Callable, das implizit als Argument die zu dekorierende Funktion bzw. Methode bzw. Klasse übergeben bekommt. Der Rückgabewert des Dekorators wird dann an den Namen der Funktion bzw. Methode bzw. Klasse gebunden. Anders ausgedrückt:

@decorator
def spam():
    pass

ist nichts anderes als syntaktischer Zucker für

def spam():
    pass
spam = decorator(spam)

Das ist auch der Grund, warum unser Beispieldekorator decorator die Funktion explizit wieder zurück gibt.

Das, was nach dem @ folgt, muss jedoch nicht zwingend ein Name sein. Vielmehr kann es ein beliebiger Ausdruck sein. Der Ausdruck wird evaluiert und das Ergebnis des Ausdrucks ist dann der Dekorator. Um das zu verdeutlichen, folgt jetzt ein Beispiel für eine Dekoratoren-Factory:

def decorator_for(name):
    """Ich bin eine Factory und ich gebe einen Dekorator zurück."""
    print "Erzeuge einen Dekorator für", name
    # Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück geben
    return decorator

@decorator_for("Csaba")
def spam():
    print "Ich bin spam."

Und wieder die Frage, was passiert, wenn man den Code ausführt?

>>> def decorator_for(name):
...     """Ich bin eine Factory und ich gebe einen Dekorator zurück."""
...     print "Erzeuge einen Dekorator für", name
...     # Den eigentlichen Dekorator (aus dem vorigen Beispiel) zurück geben
...     return decorator
...
>>> @decorator_for("Csaba")
... def spam():
...     print "Ich bin spam."
...
Erzeuge einen Dekorator für Csaba
Ich bin ein Dekorator und dekoriere spam

Und was passiert, wenn man die dekorierte Funktion ausführt?

>>> spam()
Ich bin spam.

Wie zu erwarten, noch immer nichts spektakuläres.

Wann wird ein Dekorator ausgeführt?

Der aufmerksame Leser kann sich diese Frage bereits selbst beantworten: Der Dekorator wird nicht etwa bei jedem Aufruf der dekorierten Funktion bzw. Methode aufgerufen, sondern genau ein einziges Mal, nämlich dann, wenn die Funktion bzw. Methode deklariert wird.

Ich will aber, dass bei jedem Funktionsaufruf etwas passiert

Auch das kann sich der aufmerksame Leser bereits selber herleiten: Wie bereits erwähnt, wird der Rückgabewert des Dekorators an den Namen der Funktion gebunden, die dekoriert wird. Als Beispiel also jetzt ein Dekorator, der nicht explizit die dekorierte Funktion wieder zurückgibt.

def useless_decorator(func):
    return None
>>> @useless_decorator
... def spam():
...     print "Ich bin spam."
...
>>> spam()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
>>> spam
>>>

Da der Dekorator None zurückgibt, wird auch None an den Namen der Funktion, also spam, gebunden und die ursprüngliche Funktion ging verloren.

Man kann jetzt natürlich nicht nur die Original-Funktion oder None zurück geben. Vielmehr kann man auch einen Wrapper zurück geben, der dann die ursprüngliche Funktion aufruft. Damit hat man dann das erreicht, was man wollte: Bei Jedem Funktionsaufruf soll etwas passieren.

def verbose_caller(func):
    print "Erzeuge einen Wrapper für die Funktion", func.__name__

    def wrapper():
        print "Rufe die Funktion", func.__name__, "auf"
        func()
    return wrapper
>>> @verbose_caller
... def spam():
...     print "Ich bin spam"
...
Erzeuge einen Wrapper für die Funktion spam
>>> spam()
Rufe die Funktion spam auf
Ich bin spam
>>> spam
<function wrapper at 0x7f8d95b3c758>

So gesehen ist der Dekorator in diesem Fall nichts anderes als eine Wrapper-Factory.

Da das immer noch ziemlich langweilig ist, wollen wir uns jetzt einen personalisierten Wrapper erzeugen. Dazu bauen wir eine Wrapper-Factory-Factory:

def verbose_caller_for(name):
   print "Erzeuge eine Wrapper-Factory für", name

   def verbose_caller(func):
       print "Erzeuge einen Wrapper für die Funktion", func.__name__

       def wrapper():
           print "Rufe die Funktion", func.__name__, "für", name, "auf"
           func()
       return wrapper
   return verbose_caller
>>> @verbose_caller_for("Csaba")
... def spam():
...     print "Ich bin spam"
...
Erzeuge eine Wrapper-Factory für Csaba
Erzeuge einen Wrapper für die Funktion spam
>>> spam()
Rufe die Funktion spam für Csaba auf
Ich bin spam

Da das ganze langsam doch recht unübersichtlich wird, überlegen wir uns als gute Entwickler, wie man das ganze verschönern könnte.

Klassenbasierte Dekoratoren

Dekoratoren sind einfach Callables, die das zu dekorierende Objekt entgegen nehmen. Instanzen von Klassen können jedoch ein Callable implementieren, über die spezielle Methode __call__. Warum also nicht einen Dekorator über eine Klasse implementieren? Als passionierte Java-Entwickler wissen wir, dass Klassen alles übersichtlicher machen.

class PersonalizedVerboseCaller(object):
    def __init__(self, name):
        self.name = name

    def __call__(self, func):
        return self.decorate(func)

    def decorate(self, func):
        """Wrapper factory."""
        print "Erzeuge einen Wrapper für die Funktion", func.__name__
        def wrapper():
            print "Rufe die Funktion", func.__name__, "für", self.name, "auf"
            func()
        return wrapper
>>> @PersonalizedVerboseCaller("Csaba")
... def spam():
...     print "Ich bin spam"
...
Erzeuge einen Wrapper für die Funktion spam
>>> spam()
Rufe die Funktion spam für Csaba auf
Ich bin spam

Sun 27 October 2013

Wie Peter den deutschen Mittelstand rettet

Das ist Peter[*]. Peter arbeitet beim "Zentrum für Cyber Defence". Das "Zentrum für Cyber Defence" hilft, den deutschen Mittelstand vor Industriespionage zu schützen.

Wie er das tut, wird eindrucksvoll in einer (Kurz-)Reportage des br gezeigt:

Da der Cyber aber ziemlich groß ist, kann man da leicht den Überblick verlieren. Dashboards to the rescue!

Damit man auch bei der Kaffeepause enorm wichtige Nachrichten nicht verpasst (für den unwahrscheinlichen Fall, dass in der Cafeteria kein riesiger Monitor mit dem CND Radar hängt), gibt es das ganze auch als praktische Smartphone-App.

Auch vor Deep Packet Inspection macht die Cyber-Defence-Forschung keinen Halt:

Diese sieben Minuten sollte sich jeder Zeit nehmen, um Steuergeldern beim Verbrennen zuzusehen. Daher hier noch einmal der Link zur Dokumentation.

[*]Name von der Redaktion geändert

Mon 26 August 2013

Blogneustart - Versuch drei

Nach langer Zeit der Abstinenz, unter anderem ausgelöst durch den Plattentod des Blog-Hosts, habe ich beschlossen mal wieder ein wenig zu bloggen. Wie sich das gehört habe ich also als erstes einmal den kompletten Blog umgebaut.

Was hat sich geändert?

Das Aussehen! Und mit dem Aussehen auch die verwendete Blogsoftware. Nach Drupal und schrift ist jetzt Pelican an der Reihe. Zum ersten Mal habe ich das Theme komplett alleine erstellt, mit der Hilfe von Bootstrap.

Was ist gleich geblieben?

  • Für den geneigten Leser sichtbar: Es gibt noch immer keine Kommentare. Wer das Bedürfnis hat, einen Beitrag zu kommentieren oder mir einfach so etwas mitzuteilen, mag mir bitte eine E-Mail schreiben. Meine E-Mail-Adresse kann man auf der About-Seite finden.
  • Nicht so sichtbar: Der Inhalt wird noch immer aus reStructuredText erstellt.
  • Ich habe mich bemüht, dass meine alten Beiträge erhalten bleiben. Es gibt auch einige neue öffentliche Einträge, die ich aus dem nicht-öffentlichen Archiv ausgegraben habe. Die URLs zu den einzelnen Einträgen haben sich geändert, da muss ich noch überlegen, ob es mir die Mühe wert ist, dass die alten URLs noch gehen. Da der Blog aber eine ganze Weile nicht verfügbar war und sich niemand beschwert hat, glaube ich fast nicht, dass das passieren wird.

Wird es hier in Zukunft wieder regelmäßig Inhalte geben?

Das weiß ich ehrlich gesagt noch nicht. So manchen kleinen Blogeintrag, den ich hier früher verfasst habe, würde ich heute einfach auf G+ posten. Wir werden sehen. Vorgenommen habe ich es mir jedenfalls. Ihr könnt ja den Feed mal in euren Google-Reader-Ersatz aufnehmen.

Sun 11 November 2012

Automatisiert Refleaks in Unittests erkennen

Wenn man händisch CPython-Erweiterungen mit Hilfe der C-API baut (und nicht etwa Cython oder Ähnliches benutzt), läuft man leider sehr leicht Gefahr, dass man versehentlich Reference Leaks einbaut.

Um das Auffinden von Refleaks zu erleichtern, zählt CPython bei einem Debug-Build die Summe aller Referenzen:

Python 3.4.0a0 (default:9214f8440c44, Nov 11 2012, 00:15:25)
[GCC 4.7.1] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
[60488 refs]
>>> n = 42
[60490 refs]

Die Summe der Referenzen ist die Zahl zwischen den []. In diesem Beispiel hat sich die Gesamtanzahl der Referenzen um zwei erhöht, weil die Zahl 42 an zwei Namen gebunden wurde: einmal an n und einmal an _, weil Python im interaktiven Modus immer unter _ eine Referenz auf den letzten Wert hält.

Die Gesamtanzahl der Referenzen kann man in Debug-Builds auch programmatisch über die Funktion sys.gettotalrefcount erhalten. Also liegt es nahe, dass man versucht, Refleaks automatisch zu finden, beispielsweise, indem man die Gesamtanzahl der Referenzen vor und nach einem Testlauf betrachtet.

Die Idee ist hierbei recht einfach: Wird die Test-Suite mit einem Debug-Python ausgeführt, wird jeder Test fünf mal ausgeführt. Bei den letzten zwei Durchläufen wird dann die Gesamtzahl der Referenzen vor und nach dem Test verglichen. Die drei Durchläufe vorher werden nicht ausgewertet, damit sich die Zahl der Referenzen erst auf einen Wert einpendeln kann (beispielsweise, wenn Caches im Test eine Rolle spielen etc.).

Im Folgenden eine simple Umsetzung der Idee für das klassische unittest-Modul:

hunt_leaks = hasattr(sys, "gettotalrefcount")
if hunt_leaks:
    import gc

def _is_not_suite(test):
    try:
        iter(test)
    except TypeError:
        return True
    return False


def _cleanup():
    sys._clear_type_cache()
    gc.collect()

def _hunt(test):
    def test_wrapper(*args, **kwargs):
        deltas = []
        _cleanup()
        for i in xrange(5):
            before = sys.gettotalrefcount()
            test(*args, **kwargs)
            _cleanup()
            after = sys.gettotalrefcount()
            if i > 2:
                deltas.append(after - before)
        if any(deltas):
            print("{0!r} leaks: {1}".format(test, deltas))
    return test_wrapper

class TestSuite(unittest.TestSuite):
    def __iter__(self):
        for test in super(TestSuite, self).__iter__():
            if hunt_leaks and _is_not_suite(test):
                yield _hunt(test)
            else:
                yield test

Anzumerken ist hierbei, dass bei umfangreichen Testsuites _cleanup noch erweitert werden muss. Beispielsweise muss man die ABC-Registry aufräumen, Caches (re, struct, urllib, linecache) und warnings müssen aufgeräumt werden, etc.

Jetzt muss man nur noch unittest beibringen, dass es die obige TestSuite benutzen soll. Benutzt man keinen besonderen Test-Runner, kann man das beispielsweise einfach wie folgt tun:

def test():
    loader = unittest.TestLoader()
    loader.suiteClass = TestSuite
    unittest.main(testLoader=loader)

if __name__ == "__main__":
    test()

Reference Leaks können natürlich auch mit "reinem" Python-Code passieren, indem man globalen Zustand verändert. Ein etwas konstruiertes Beispiel:

class Observable(object):
    def __init__(self):
        self.observers = []

    def add_observer(self, callable):
        self.observers.append(callable)

    def notify_observers(self, value):
        for observer in self.observers:
            observer(value)

value = Observable()

class SpamTest(unittest.TestCase):
    def test_observable(self):
        class Observer(object):
            def __init__(self):
                self.called = False

            def __call__(self, value):
                self.called = True

        observer = Observer()
        value.add_observer(observer)
        value.notify_observers(42)
        self.assertTrue(observer.called)

Führt man die Tests jetzt aus, führt das zu folgender Ausgabe:

.....<__main__.SpamTest testMethod=test_observable> leaks: [40, 40]

----------------------------------------------------------------------
Ran 5 tests in 0.007s

Jetzt weiß man zumindest, dass der Test test_observable leckt. Was genau leckt, muss man aber immer noch selbst herausfinden. Was nicht unbedingt immer leicht und offensichtlich ist.