Making Statik Backwards-Compatible

By Thane on Mon 21 November 2016

Up to today, Statik only worked with Python 3.5+. As of v0.8.0, it has been tested to work with Python 2.7 (specifically, Python 2.7.12). Statik makes use of many Python 3-specific syntax conventions. One of the best resources I found was the documentation for the Python future library. My main resource there was their Cheat Sheet: Writing Python 2-3 compatible code.

Unicode

The first challenge was to ensure that Python 2 and 3 code both use Unicode encoding for strings by default. Fortunately, back-porting Python 3 code to Python 2 code that chooses Unicode strings by default is pretty easy. Simply include the following import right at the beginning of your script:

from __future__ import unicode_literals

There were several places where this didn't work, however. In one place I was dynamically creating a Python type using the type() built-in function:

Model = type(
    model.name,   # Threw an exception: requires a string, not a unicode value
    (Base, ),
    model_fields
)

In order to fix this, I had to cast this value to a string using the native str() function (not the one from the builtins library in python-future, because that one forces the string to Unicode):

Model = type(
    str(model.name),
    (Base, ),
    model_fields
)

The other place where this didn't work so well was in my setup.py script, which complained about the package field being of the wrong data type. In this case, I just made sure to leave out the unicode_literals import and let the strings be defined as standard Python strings. See the setup.py script on GitHub.

For loops

I like how concise Python 3's for loop iteration is:

# In Python 3
for key, value in some_dict.items():
    do_something(key, value)

Trouble is, according to Python-Future's cheat sheet, this is inefficient in Python 2. I suppose it would have worked, but I don't like writing code using functions that I know are inefficient when a more efficient mechanism exists. Therefore, one of the possible workarounds I used is the following:

from future.utils import iteritems

for key, value in iteritems(some_dict):
    do_something(key, value)

Calling parent class constructors

I was quite sad to do away with Python 3's more elegant syntax for calling a parent class' constructor:

class ParentClass(object):
    def __init__(self):
        do_something_parenty()

class ChildClass(ParentClass):
    def __init__(self):
        super().__init__()
        do_something_childy()

Instead, I had to revert to Python 2's more verbose form, which is still supported in Python 3:

class ParentClass(object):
    def __init__(self):
        do_something_parenty()

class ChildClass(ParentClass):
    def __init__(self):
        super(ChildClass, self).__init__()
        do_something_childy()

Dynamic Module Imports

Python 3 has really great built-in support for dynamically loading modules through the importlib library, which was used to dynamically import template tags and filters for Statik projects (added in this pull request by @pztrick).

But how can one achieve this same functionality using Python 2? Unfortunately, Python 2's importlib is a very lightweight wrapper around the __import__ library, and doesn't provide nearly as much functionality as Python 3's version. In the end, I had to make use of the six library to do Python 2/3 detection and default to the built-in imp library in Python 2 (deprecated in Python 3):

import six

if six.PY3:
    import importlib.util
elif six.PY2:
    import imp


def import_module(module_name, path):
    """Import the given module from the specified file system path."""
    if six.PY3:
        spec = importlib.util.spec_from_file_location(module_name, path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
    elif six.PY2:
        imp.load_source(module_name, path)

This was probably the most obscure part of the porting process (I suppose it's pretty uncommon to be dynamically loading libraries in this particular way).

Travis CI

As an additional plus, I've integrated Statik with Travis CI to build the code automatically as I push to master. The great things about Travis CI are that it's free for open source projects, and it's super easy to build your Python project against multiple different versions of Python. See their Building a Python Project guide.

At the time of this writing, I build and run my unit tests against Python 2.7 and 3.5. This is the .travis.yml file at the time of this writing:

language: python
python:
  - "2.7"
  - "3.5"
install:
  - "pip install -r requirements.txt"
script:
  - "python -m unittest discover"

Conclusion

It takes quite a bit of work to port a Python 3 project to be backwards-compatible with Python 2, but it's apparently more difficult to do it the other way around in certain cases (2 to 3).

I'm just glad Statik can now be used in a wider range of projects, as it can now be embedded into Python 2.7+ projects.

Hope it helps!

Comments