Now it's your turn

Posted in:

I just watched Jacob's talk on "Porting Django apps to Python 3", and realised it was time to tackle my own small Django apps.

The problem with porting is that you need your dependencies to be ported first. But now that Django has Python 3 support, the finger is no longer pointing at Django – it is pointing at all of us with Django apps that have only Django as a dependency (or other dependencies that are already ported to Python 3). As Jacob put it at the end of the talk, it’s your turn.

So, I took the challenge, and here is a walk through of what you need to do, and what I had to do:

  1. Find an app/library you've written that has very few dependencies, or all dependencies already ported to Python 3. In my case, django-easyfilters. Not a massively popular library, but it has had over 1000 downloads, and I know some people use it.

  2. Install tox and create a tox.ini file to run your test suite on more than one version. Start with all the Django versions you want to support, with Python 2.x combinations (Python 2.6 and 2.7 recommended), and Python 3.3.

    My tox.ini file looks like this.

  3. Run tox and watch it pass with Python 2.x.

    Of course, if you get failures at this point, fix them first. Because I am an exceptionally good boy, I got no failures, even for Django 1.5 which I had not tried before with this library. This is my reward for having Done Things Right (with this particular app :-). As well as a pretty complete test suite, I even created a small demo app, and left instructions about how to use it. I may be the only person to have ever read these instructions, but it was well worth it.

  4. Watch the tests fail with Python 3.3

    OK, now to start fixing it.

    Activate the Python 3.3 virtualenv that tox created. In my case:

    . .tox/py33-django15/bin/activate

    Then run your test command. In my case:

    ./manage.py test django_easyfilters

    You may need to do some work even to get it to run at all.

    On the first run, out of 45 tests only 7 passed. Gulp.

    Now go read a porting guide, using the 'single source' method, and check out Armin’s Python 3 porting redux.

    You'll probably want to install 'six' and add it to your project's dependencies (unless you are targetting only Django 1.4 and later, in which case you can use django.utils.six). And add that dependency to your setup.py file, and your tox.ini file, and set tox running, because it will need to rebuild all your virtualenvs if six wasn't a dependency before.

    Now iterate on fixing your tests. Each time you find a problem, grep the code base for other instances of it.

    I found a relatively small bunch of problems which caused most of my tests to fail:

    • Use of implicit relative imports

    • Use of Decimal._rescale which is removed in Python 3.3

    • map() had to be replaced with list(map()) a few times

    • I needed from six.moves import xrange to use xrange

    • s/unicode/six.text_type/ and similar

    • I copied python_2_unicode_compatible from Django 1.5 for fixing __unicode__ and __str__:

      def python_2_unicode_compatible(klass):
          """
          A decorator that defines __unicode__ and __str__ methods under Python 2.
          Under Python 3 it does nothing.
      
          To support Python 2 and 3 with a single code base, define a __str__ method
          returning text and apply this decorator to the class.
          """
          if not six.PY3:
              klass.__unicode__ = klass.__str__
              klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
          return klass
      
    • __cmp__ no longer supported.

      This was a bit of pain to fix. For some of my classes __cmp__ was a much more natural way to define sorting, and it broke my head trying to rewrite. So I did this:

      if six.PY3:
          # Support for __cmp__ implementation below
          def cmp(a, b):
              return (a > b) - (a < b)
          from functools import total_ordering
      else:
          total_ordering = lambda c: c # no-op
      

      And in the classes, just added this:

      @total_ordering
      class MyClass(object):
      
             # ...
      
             def __eq__(self, other):
                 return self.__cmp__(other) == 0
      
             def __lt__(self, other):
                 return self.__cmp__(other) < 0
      

      You could even write a decorator to do all of this.

      When I migrate fully away from Python 2.x I may get round to rewriting these.

  5. You also need to check your setup.py file. If you use setuptools, it should work fine - under Python 3 this is supported by 'distribute', a setuptools fork that is a drop-in replacement. Tools like pip and virtualenv install distribute for Python 3 environments. But you do need to check you don't have syntax errors under Python 3.

    Finally, add the Python 3.3 trove category to your setup.py:

    "Programming Language :: Python :: 3.3",

Overrall, this took me about 2.5 hours to complete. However, the app is small, and has a pretty good test suite, which makes things much, much easier. On the other hand, I've given you a clear plan of attack, which is a big part of the battle.

Now it's your turn

Comments §

Comments should load when you scroll to here...