Taavi's Blog


Egg Peggery, Shoulds, and Reality

2011-08-09T00:21:56-0400 | categories: debugging, programming

Last Saturday I attented a learn-in for Lernanta. Lernanta is the new project that runs p2pu.org (forked from Batucada which runs Mozilla's drumbeat.org). After getting the development environment set up (it's easy in an Ubuntu VM), I picked my first ticket: allow password resets via username as well as email address.

After looking at the source to see how password resets work, and asking on IRC regarding how it should work (Are usernames and email addresses disjunct sets? No.), I went to write a failing test. But on running the test suite, a full half of the tests broke with fairly catastrophic errors:

Traceback (most recent call last):
  File "/mnt/host/lernanta/../lernanta/apps/users/tests.py", line 63, in test_unauthenticated_redirects
    response = self.client.get(full)
…
  File "/mnt/host/lernanta/../lernanta/urls.py", line 5, in <module>
    admin.autodiscover()
  File "/home/taavi/lernanta-env/lib/python2.7/site-packages/django/contrib/admin/__init__.py", line 26, in autodiscover
    import_module('%s.admin' % app)
  File "/home/taavi/lernanta-env/lib/python2.7/site-packages/django/utils/importlib.py", line 35, in import_module
    __import__(name)
  File "/mnt/host/lernanta/../lernanta/apps/drumbeat/admin.py", line 21, in <module>
    Tag, Resource, Vote, Site])
  File "/home/taavi/lernanta-env/lib/python2.7/site-packages/django/contrib/admin/sites.py", line 112, in unregister
    raise NotRegistered('The model %s is not registered' % model.__name__)
NotRegistered: The model Group is not registered

Searching for NotRegistered didn't turn up anything useful. Most errors people tended to see had to do with doubly-registering models, and those didn't appear related to the problem at hand.

Looking at the admin modules in Lernanta turned up something interesting. The drumbeat app was trying to unregister models like Group. Apparently this is because—in the context of drumbeat—those bits of django.contrib.auth aren't interesting and just contribute visual clutter. But the admin worked via the web interface, just not in tests. Why would things be defined properly in production use, but not in test? I searched for information about INSTALLED_APPS ordering, but the only messages I could find indicated that order shouldn't matter. But I had a feeling it did anyway. How could it not, given the code actually in the various admin.py files? Paul confirmed that order matters.

So I started dumping the order of loading the various admin modules in django.contrib.admin.__init__.autodiscover:

diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index 2597414..26db254 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -27,6 +27,8 @@ def autodiscover():
     from django.utils.importlib import import_module
     from django.utils.module_loading import module_has_submodule

+    import pprint
+    pprint.pprint(settings.INSTALLED_APPS)
     for app in settings.INSTALLED_APPS:
         mod = import_module(app)
        # Attempt to import the app's admin module.

Lernanta's python manage.py test command uses nose under the covers, which captures logging and standard out while running tests and will print the contents on failure. I also used the -x flag to stop on first failure, so I didn't have to wait or wade through dozens of failures.

Through this I found that the order of settings.py was being changed! Once we had that figured out, Zuzel quickly pinpointed the problem in django-nose introduced on July 19th where INSTALLED_APPS is cast into a set(). I'm a bit embarassed that I didn't find that reference myself (I'd been suspecting a rogue call to set()), but my experience with pip (used to install Lernanta's dependencies) is limited at this point, and I never expected to find code in lernanta-env/src!

Rolling back to an older version of django-nose fixed the test failures. And at this point there's a new version of django-nose that doens't suffer from this problem.

Which brings me to egg peggery. If you're writing a Python library, you very probably don't want to peg your requirements to specific versions, because your consumer might need something different. But if you're writing an end-user app (like Lernanta), I highly suggest pegging all versions of all the dependencies in your virtualenv. pip encourages you to peg your dependencies' versions! If you don't, you will have no guarantee that installing your app next week will still work. The reality is that things change, sometimes breaking your assumptions. Don't assume more than you have to.

Explicit is better than implicit!