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
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
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.
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
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
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.