A couple quick tips
As noted the other day, I’ve spent the last little while working to freshen up various bits of open-source code I maintain, in order to make sure all of them have at least one recent release. Along the way I’ve picked up a few little tips and tricks; some of them may be old hat to you if you’ve been lucky enough to be working with fairly modern Django and Python versions for a while, but I think they’re still worth sharing. Today I’d like to dive into two in particular that are relevant for people who write and distribute Django applications.
Tell people about your Django version compatibility!
One big complaint that I’ve heard several times — and run into more than once myself — about the Django application ecosystem is that it’s sometimes difficult to figure out which versions of Django a particular application is compatible with. The obvious place to mention this is in an application’s documentation (and if you’re a Django application author and you don’t have some documentation, you are, as the kids say these days, doing it wrong). Usually I try to make sure Django version compatibility (and Python version compatibility) gets mentioned at least once in the documentation for anything I write; for example, django-flashpolicies lists Django and Python version compatibility in its installation instructions. And just to be extra careful, it provides that information again in its FAQ.
And while you should have this mentioned somewhere in your documentation (my preference is to do it in a “how to install this” document), there’s one other place where you can and should mention Django version compatibility: in your trove classifiers.
Trove classifiers are part of your application’s
setup.py file, and are used to provide some extra metadata about your code, which can then be displayed, filtered, searched on, etc. by users of the Python Package Index or other tools which work with packaged Python code. PyPI publishes a full list of current trove classifiers, and making good use of them can help significantly with your application’s discoverability.
But there are two sets of classifiers in particular that I want to encourage people to use. One set indicates Python version compatibility, and lets you indicate a package that’s Python-2-only, a package that’s Python-3-only, a package that works on both, and which specific releases your package works on. Going back to the example of
django-flashpolicies, it includes these trove classifiers in its setup.py:
'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4',
This tells anyone who’s searching/filtering (and PyPI has an interface for doing that) that
django-flashpolicies works on both Python 2 and Python 3, and specifically supports Python 2.7, 3.3 and 3.4 (the same set of versions supported by the last two releases of Django itself).
For years now, PyPI has also had a trove classifier to indicate a package is meant to work with Django (the classifier is
Framework :: Django). But recently, the kind folks who run PyPI added an extra set of classifiers to indicate Django version compatibility. Which means
django-flashpolicies can also include this information in its
'Framework :: Django :: 1.7', 'Framework :: Django :: 1.8',
And voilà! Now we don’t even have to turn to the documentation (though this still should be mentioned in your documentation) to find out whether something is compatible with the latest version of Django — we can see it right from the listing page on PyPI (under “Categories”), and best of all those classifiers turn into links which can be used to filter for packages. So if you want to see all packages compatible with Django 1.8, you can, and if you’re like me and you’re running Django 1.8 on Python 3.4, you can see all packages compatible with both of those.
So next time you’re packaging up a Django application for others to use, remember to set the correct trove classifiers in your
setup.py — I, and many other people, will thank you for it.
Make it easy to run your tests
The Python ecosystem is fortunate to have an almost embarrassing number of good testing tools, so for most types of Python-based projects the difficult part is choosing which great testing tool you’ll use. Distributing a Django application with tests can be slightly trickier, though, because even though Django has good built-in testing tools (and is easy to integrate with popular third-party testing frameworks), you need to configure Django in order to run the tests.
When you’re just running your test suite locally on your own computer, this isn’t so bad; you can set up a virtualenv with a bare-bones Django project in it, and use
manage.py test. When you’re trying to distribute an application to other people, and especially when you’re trying to use online continuous-integration services like Travis CI, though, that’s not an option.
Fortunately, Django is easy to configure in “standalone” mode, and easy to configure in a way that’s not dependent on the specifics of the system it will run on. The particular two-step involved here is actually a bit more than two steps, but it’s still fairly simple:
- Create a dictionary containing the settings you’ll need.
- Call Django’s settings.configure(), passing in the dictionary of settings as keyword arguments.
- Call django.setup() to load your settings and populate Django’s application registry.
- Instantiate a test runner and invoke it.
All of this is covered in Django’s own testing documentation, which shows how easy it is to create a single file to run your application’s tests.
One thing not mentioned there is that it’s also easy to provide a standard and non-Django-specific entry point to running your tests. For years, the
setuptools project has provided the ability to specify a test runner in a package’s
setup.py file, which makes running tests as easy as
setup.py test. Historically I’ve avoided
setuptools because of a lot of baggage and (in my opinion) poor design decisions, but the state of Python packaging has significantly improved in recent years, to the point where I’m comfortable making use of
setuptools at least for this particular convenience. The result is that, once you write your standalone test-runner script (by the convention in the Django documentation, called
runtests.py), you can simply add the relevant bits to your application’s
setup.py file and suddenly
setup.py test will just work:
from setuptools import setup setup(name="yourapp", zip_safe=False, # …other options here… packages=['yourapp', 'yourapp.migrations', 'yourapp.tests'], test_suite=['yourapp.runtests.run_tests'] )
Other than changing the import to be
from setuptools import setup instead of
from distutils.core import setup, there is one important change to make when doing this: the
zip_safe=False argument, which is
setuptools-specific, and ensures that
setuptools won’t try to distribute or install your package as a zipped “egg” file (this would be an example of the negative baggage
setuptools brings — eggs were an attempt to emulate Java’s JAR as a single-file distribution format, and only really succeeded at being confusing and requiring everyone else to implement workarounds for accidental egg installation). And while Django can handle an application installed as an egg, I strongly recommend not using or allowing them, for a few reasons:
- They’re sufficiently different from everything else in the standard Python ecosystem that they’ll just confuse your users.
- If you need a simple single-file, pre-built distribution format, wheel is a better solution and has support in standard Python packaging tools.
- Third-party packages may not be expecting or able to work properly with your application if it’s distributed/installed as an egg.
- And Django itself will probably drop support for eggs in the near future.
At any rate, being able to simply use
setup.py test is pretty compelling, and makes the process of documenting how to run your tests, or integrating with online testing tools, a whole lot easier. For reference, here’s a full example of a runtests.py file (from django-project-portfolio), and here’s a full example setup.py using it.