diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3d4d505204e049c862039cbce1b1bc2921687e6d..243db5f1ee6a57c7490e1f1b81814f36a8aaaa1d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,17 +1,380 @@
-test:2.7:
-  image: fsleyes-py27
+###########################################################################
+# This file defines the build process for fslpy, as hosted at:
+#
+#    https://git.fmrib.ox.ac.uk/fsl/fslpy
+#
+# The build pipeline comprises four stages:
+#
+#    1. test:   Unit tests
+#
+#    2. doc:    Building API documentation
+#
+#    3. build:  Building source distributions and wheels
+#
+#    4. deploy: Uploading the build outputs to pypi, and the documentation
+#               to a hosting server.
+#
+# The test stage is executed on all branches of upstream and fork
+# repositories.
+#
+# The doc stage, and the deploy-doc job, is executed on all branches of the
+# upstream repository.
+#
+# The build stage, and the remaining jobs in the  deploy stage, are only
+# executed on the upstream repository, and only for release tags.
+#
+# The deploy stages are manually instantiated.
+###########################################################################
+
+
+stages:
+ - test
+ - doc
+ - build
+ - deploy
+
+
+###############################################################################
+# A number of variables must be set for the jobs to work. The following
+# variables are implicitly defined in any gitlab CI job:
+#
+#   - CI_PROJECT_PATH    - gitlab namespace/project
+#   - CI_COMMIT_REF_NAME - branch name, provided by gitlab
+#   - CI_COMMIT_TAG      - present if build is running on a tag
+#
+# These variables must be explicitly set as "secret" variables:
+#
+#   - SSH_PRIVATE_KEY_GIT            - private key for git login to remote host
+#                                      (UPSTREAM_URL)
+#
+#   - SSH_PRIVATE_KEY_FSL_DOWNLOAD   - private key for downloading some FSL
+#                                      files from a remote server (FSL_HOST)
+#
+#   - SSH_PRIVATE_KEY_DOC_DEPLOY     - private key for rsyncing documentation
+#                                      to remote host (DOC_HOST)
+#
+#   - SSH_SERVER_HOSTKEYS            - List of trusted SSH hosts
+#
+#   - TWINE_PASSWORD:                - Password to use when uploading to pypi
+###############################################################################
+
+
+variables:
+  UPSTREAM_PROJECT:     "fsl/fslpy"
+  UPSTREAM_URL:         "git@git.fmrib.ox.ac.uk"
+  DOC_HOST:             "paulmc@jalapeno.fmrib.ox.ac.uk"
+  FSL_HOST:             "paulmc@jalapeno.fmrib.ox.ac.uk"
+  TWINE_USERNAME:       "pauldmccarthy"
+  TWINE_REPOSITORY_URL: "https://testpypi.python.org/pypi"
+
+
+####################################
+# These anchors are used to restrict
+# when and where jobs are executed.
+####################################
+
+
+.only_upstream: &only_upstream
+  only:
+    - branches@fsl/fslpy
+
+
+.only_master: &only_master
+  only:
+    - master@fsl/fslpy
+
+
+.only_releases: &only_releases
+  only:
+    - tags@fsl/fslpy
+
+
+.except_releases: &except_releases
+  except:
+    - tags
+
+
+##########################################################
+# The setup_ssh anchor contains a before_script section
+# which does the following:
+#
+#  - Sets up key-based SSH login, and
+#    installs the private keys, so
+#    we can connect to servers.
+#
+#  - Configures git, and adds the
+#    upstream repo as a remote
+#
+# (see https://docs.gitlab.com/ce/ci/ssh_keys/README.html)
+#
+# NOTE: It is assumed that non-docker
+#       executors are already configured
+#       (or don't need any configuration).
+##########################################################
+
+
+.setup_ssh: &setup_ssh
+  before_script:
+
+    - if [[ -f /.dockerenv ]]; then
+
+        apt-get update -y                 || yum -y check-update           || true;
+        apt-get install -y openssh-client || yum install -y openssh-client || true;
+        apt-get install -y rsync          || yum install -y rsync          || true;
+
+        eval $(ssh-agent -s);
+        mkdir -p $HOME/.ssh;
+
+        echo "$SSH_PRIVATE_KEY_GIT"          > $HOME/.ssh/id_git;
+        echo "$SSH_PRIVATE_KEY_FSL_DOWNLOAD" > $HOME/.ssh/id_fsl_download;
+
+        if [[ "$CI_PROJECT_PATH" == "$UPSTREAM_PROJECT" ]]; then
+          echo "$SSH_PRIVATE_KEY_DOC_DEPLOY" > $HOME/.ssh/id_doc_deploy;
+        fi;
+
+        chmod go-rwx $HOME/.ssh/id_*;
+
+        ssh-add $HOME/.ssh/id_git;
+        ssh-add $HOME/.ssh/id_fsl_download;
+
+        if [[ "$CI_PROJECT_PATH" == "$UPSTREAM_PROJECT" ]]; then
+          ssh-add $HOME/.ssh/id_doc_deploy;
+        fi
+
+        echo "$SSH_SERVER_HOSTKEYS" > $HOME/.ssh/known_hosts;
+
+        touch $HOME/.ssh/config;
+
+        echo "Host ${UPSTREAM_URL##*@}"                    >> $HOME/.ssh/config;
+        echo "    User ${UPSTREAM_URL%@*}"                 >> $HOME/.ssh/config;
+        echo "    IdentityFile $HOME/.ssh/id_git"          >> $HOME/.ssh/config;
+
+        echo "Host docdeploy"                              >> $HOME/.ssh/config;
+        echo "    HostName ${DOC_HOST##*@}"                >> $HOME/.ssh/config;
+        echo "    User ${DOC_HOST%@*}"                     >> $HOME/.ssh/config;
+        echo "    IdentityFile $HOME/.ssh/id_doc_deploy"   >> $HOME/.ssh/config;
+
+        echo "Host fsldownload"                            >> $HOME/.ssh/config;
+        echo "    HostName ${FSL_HOST##*@}"                >> $HOME/.ssh/config;
+        echo "    User ${FSL_HOST%@*}"                     >> $HOME/.ssh/config;
+        echo "    IdentityFile $HOME/.ssh/id_fsl_download" >> $HOME/.ssh/config;
+
+        echo "Host *"                                      >> $HOME/.ssh/config;
+        echo "    IdentitiesOnly yes"                      >> $HOME/.ssh/config;
+
+        git config --global user.name  "Gitlab CI";
+        git config --global user.email "gitlabci@localhost";
+
+        if [[ `git remote -v` == *"upstream"* ]]; then
+            git remote remove upstream;
+        fi;
+        git remote add upstream "$UPSTREAM_URL:$UPSTREAM_PROJECT";
+      fi
+
+
+###################################################
+# The patch_version anchor contains a before_script
+# section which is run on release builds, and makes
+# sure that the version in the code is up to date
+# (i.e. equal to the tag name).
+###################################################
+
+
+.patch_version: &patch_version
+  before_script:
+
+    - if [[ "x$CI_COMMIT_TAG" != "x" ]]; then
+        echo "Release detected - patching version - $CI_COMMIT_REF_NAME";
+        python -c "import fsl.version as v; v.patchVersion('fsl/version.py', '$CI_COMMIT_REF_NAME')";
+      fi
+
+
+############
+# Test stage
+############
+
+
+.test: &test_template
+  stage: test
+  <<: *setup_ssh
+
+  # Releases are just tags on a release
+  # branch, so we don't need to test them.
+  <<: *except_releases
+
+  tags:
+    - docker
+
   script:
-    - cat requirements.txt | xargs -n 1 pip install
+
+    # If running on a fork repository, we merge in the
+    # upstream/master branch. This is done so that merge
+    # requests from fork to the parent repository will
+    # have unit tests run on the merged code, something
+    # which gitlab CE does not currently do for us.
+    - if [[ "$CI_PROJECT_PATH" != "$UPSTREAM_PROJECT" ]]; then
+        git fetch upstream;
+        git merge --no-commit --no-ff upstream/master;
+      fi;
+
+    # I am currently assuming that we are
+    # running a debian 8/jessie container
+    # (the python:2.7 and 3.6 images are
+    # based on this).
+    # We need to install xvfb, and all of
+    # the wxpython dependencies.
+    - apt-get update  -y
+    - apt-get install -y xvfb libgtk-3-0
+    - apt-get install -y libnotify4 freeglut3 libsdl1.2debian
+
+    # Linux builds for wxPython are currently not
+    # on pypi, but are available at this url.
+    - pip install -f https://wxpython.org/Phoenix/release-extras/linux/gtk3/debian-8/ wxpython
+
+    # All other deps can be installed as
+    # normal. scipy is required by nibabel,
+    # but not listed in its requirements.
+    # We install test dependencies through
+    # pip, because if we let setuptools do
+    # it, it will build/install everything
+    # from source, rather than using wheels.
+    - pip install -r requirements.txt
     - pip install scipy
-    - pip install coverage
-    - su -s /bin/bash -c "xvfb-run python setup.py test" nobody
-    - coverage report -m
+    - pip install sphinx sphinx-rtd-theme
+    - pip install pytest pytest-cov pytest-html pytest-runner mock coverage
+
+    # We need the FSL atlases for the atlas
+    # tests, and need $FSLDIR to be defined
+    - export FSLDIR=/fsl/
+    - mkdir -p $FSLDIR/data/
+    - rsync -rv "fsldownload:data/atlases/" "$FSLDIR/data/atlases/"
+
+    # Finally, run the damned tests.
+
+    # We run some tests under xvfb-run
+    # because they invoke wx. Sleep in
+    # between, otherwise xvfb gets upset.
+    - xvfb-run python setup.py test --addopts="tests/test_async.py"
+    - sleep 5
+    - xvfb-run python setup.py test --addopts="tests/test_platform.py"
+
+    # We run the immv/imcpy tests as the nobody
+    # user because some tests expect permission
+    # denied errors when looking at files, and
+    # root never gets denied. Make everything in
+    # this directory writable by anybody (which,
+    # unintuitively, includes nobody)
+    - chmod -R a+w `pwd`
+    - su -s /bin/bash -c "python setup.py test --addopts='tests/test_immv_imcp.py'" nobody
+
+    # All other tests can be run as normal
+    - python setup.py test --addopts="--ignore=tests/test_async.py --ignore=tests/test_platform.py --ignore=tests/test_immv_imcp.py"
+    - python -m coverage report
+
+
+test:2.7:
+  <<: *test_template
+  image: python:2.7
+
+
+test:3.4:
+  <<: *test_template
+  image: python:3.4
+
+
+# a wxphoenix/3.5 build
+# is not yet available
+# test:3.5:
+#   <<: *test_template
+#   image: python:3.5
+
+
+# a wxphoenix/3.6 build
+# is not yet available
+# test:3.6:
+#   <<: *test_template
+#   image: python:3.6
+
+
+###########
+# Doc stage
+###########
+
+build-doc:
+  <<: *only_upstream
+  <<: *patch_version
+
+  tags:
+   - docker
+
+  stage: doc
+  image: python:3.5
 
-test:3.6:
-  image: fsleyes-py36
   script:
-    - cat requirements.txt | xargs -n 1 pip install
-    - pip install scipy
-    - pip install coverage
-    - su -s /bin/bash -c "xvfb-run python setup.py test" nobody
-    - coverage report -m
+    - python setup.py doc
+    - mv doc/html doc/"$CI_COMMIT_REF_NAME"
+  artifacts:
+    paths:
+      - doc/$CI_COMMIT_REF_NAME
+
+
+#############
+# Build stage
+#############
+
+
+build-dist:
+  <<: *only_releases
+  <<: *patch_version
+
+  stage: build
+  image: python:3.5
+
+  script:
+   - pip install wheel
+   - python setup.py sdist
+   - python setup.py bdist_wheel
+
+  artifacts:
+    paths:
+      - dist/*
+
+
+##############
+# Deploy stage
+##############
+
+
+deploy-doc:
+  <<: *only_upstream
+  <<: *setup_ssh
+  stage: deploy
+  when:  manual
+  image: python:3.5
+
+  tags:
+    - docker
+
+  dependencies:
+    - build-doc
+
+  script:
+    - rsync -rv doc/"$CI_COMMIT_REF_NAME" "docdeploy:"
+
+
+deploy-pypi:
+  <<: *only_releases
+  <<: *setup_ssh
+  stage: deploy
+  when:  manual
+  image: python:3.5
+
+  tags:
+    - docker
+
+  dependencies:
+    - build-dist
+
+  script:
+    - pip install setuptools wheel twine
+    - twine upload dist/*
diff --git a/MANIFEST.in b/MANIFEST.in
index 43a8f2413ac698be518bddfce023c8de834c71bc..bbc7ab460f7f8b4ab9b1b2d0ba261621b3825502 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,5 @@
 include           LICENSE
 include           COPYRIGHT
-include           README.md
 include           requirements.txt
 include           pytest.ini
 recursive-include doc      *
diff --git a/README.md b/README.md
deleted file mode 100644
index b79542f1663cb2d807c408f8f6630b40e72e277e..0000000000000000000000000000000000000000
--- a/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-fslpy
-=====
-
-
-The `fslpy` project is a [FSL](http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/)
-programming library written in Python. It is used by
-[FSLeyes](https://git.fmrib.ox.ac.uk/paulmc/fsleyes/).
-
-
-Dependencies
-------------
-
-
-All of the dependencies of `fslpy` are listed in the
-[requirements.txt](requirements.txt) file. Some `fslpy` modules require
-[wxPython](http://www.wxpython.org) 3.0.2.0.
-
-
-Documentation
--------------
-
-`fslpy` is documented using [sphinx](http://http://sphinx-doc.org/). You can
-build the API documentation by running:
-
-    python setup.py doc
-
-The HTML documentation will be generated and saved in the `doc/html/` directory.
-
-
-If you are interested in contributing to `fslpy`, check out the [contributing
-guide](doc/contributing.rst).
-
-
-Tests
------
-
-Run the test suite via:
-
-    python setup.py test
-
-A test report will be generated at `report.html`, and a code coverage report
-will be generated in `htmlcov/`.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a23c2efa038d259755064d992a570e03cc19e58b
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,50 @@
+fslpy
+=====
+
+
+.. image:: https://git.fmrib.ox.ac.uk/fsl/fslpy/badges/master/build.svg
+   :target: https://git.fmrib.ox.ac.uk/fsl/fslpy/commits/master/
+
+.. image:: https://git.fmrib.ox.ac.uk/fsl/fslpy/badges/master/coverage.svg
+   :target: https://git.fmrib.ox.ac.uk/fsl/fslpy/commits/master/
+
+
+The ``fslpy`` project is a `FSL <http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/>`_
+programming library written in Python. It is used by `FSLeyes
+<https://git.fmrib.ox.ac.uk/paulmc/fsleyes/>`_.
+
+
+Dependencies
+------------
+
+
+All of the dependencies of ``fslpy`` are listed in the `requirements.txt
+<requirements.txt>`_ file. Some ``fslpy`` modules require `wxPython
+<http://www.wxpython.org>`_ 3.0.2.0 or higher.
+
+
+Documentation
+-------------
+
+``fslpy`` is documented using `sphinx <http://http://sphinx-doc.org/>`_. You
+can build the API documentation by running::
+
+    python setup.py doc
+
+The HTML documentation will be generated and saved in the ``doc/html/``
+directory.
+
+
+If you are interested in contributing to ``fslpy``, check out the
+`contributing guide <doc/contributing.rst>`_.
+
+
+Tests
+-----
+
+Run the test suite via::
+
+    python setup.py test
+
+A test report will be generated at ``report.html``, and a code coverage report
+will be generated in ``htmlcov/``.
diff --git a/doc/contributing.rst b/doc/contributing.rst
index e2b1194869df1aa1db5feb76c4a7be26527681d2..8712dc6b0eb1a893356a36653a9d83a9f75e0390 100644
--- a/doc/contributing.rst
+++ b/doc/contributing.rst
@@ -45,6 +45,12 @@ numbers::
   backwards-incompatible changes.
 
 
+The version number in the ``master`` branch should be of the form
+``major.minor.patch.dev``, to indicate that any releases made from this branch
+are development releases (although development releases are not part of the
+release model).
+
+
 Releases
 --------
 
@@ -72,12 +78,10 @@ Testing
 
 
 Unit and integration tests are currently run with ``py.test`` and
-``coverage``. We don't have CI configured yet, so tests have to be run
-manually.
+``coverage``.
 
 - Aim for 100% code coverage.
-- Tests must pass on both python 2.7 and 3.5
-- Tests must pass on both wxPython 3.0.2.0 and 4.0.0
+- Tests must pass on python 2.7, 3.4, 3.5, and 3.6
 
 
 Coding conventions
diff --git a/fsl/utils/memoize.py b/fsl/utils/memoize.py
index 1a95638c87ca3c862c84307ef9b9b8a3fbb9e62b..8f56a1a61eb9fa3346da69a27d3da13fdccc0b5d 100644
--- a/fsl/utils/memoize.py
+++ b/fsl/utils/memoize.py
@@ -55,14 +55,14 @@ def memoize(func):
         try:
             result = cache[key]
 
-            log.debug('Retrieved from cache[{}]: {}'.format(key, result))
+            log.debug(u'Retrieved from cache[{}]: {}'.format(key, result))
 
         except KeyError:
 
             result     = func(*a, **kwa)
             cache[key] = result
 
-            log.debug('Adding to cache[{}]: {}'.format(key, result))
+            log.debug(u'Adding to cache[{}]: {}'.format(key, result))
 
         return result
     return wrapper
@@ -82,8 +82,15 @@ def memoizeMD5(func):
 
         hashobj = hashlib.md5()
 
+        # Convert each arg to a string
+        # representation, then encode
+        # it into a sequence of (utf-8
+        # compatible) bytes , and take
+        # the hash of those bytes.
         for arg in args:
-            arg = six.u(str(arg)).encode('utf-8')
+            if not isinstance(arg, six.string_types):
+                arg = str(arg)
+            arg = arg.encode('utf-8')
             hashobj.update(arg)
 
         digest = hashobj.hexdigest()
@@ -94,7 +101,7 @@ def memoizeMD5(func):
 
         result = func(*args, **kwargs)
 
-        log.debug('Adding to MD5 cache[{}]: {}'.format(
+        log.debug(u'Adding to MD5 cache[{}]: {}'.format(
             digest, result))
 
         cache[digest] = result
diff --git a/fsl/version.py b/fsl/version.py
index 9acc9d7fd9b8247e8cf26252b5e34c05df77064f..68a81a1e9b28ccc8f8b2f9072ed28dad62f06217 100644
--- a/fsl/version.py
+++ b/fsl/version.py
@@ -5,7 +5,7 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 """The primary purpose of this module is as a container for the ``fslpy``
-version number. A couple of conveniense functions for comparing version
+version number. A handful of convenience functions for managing version
 numbers are also defined here.
 
 .. autosummary::
@@ -14,10 +14,13 @@ numbers are also defined here.
    __version__
    parseVersionString
    compareVersions
+   patchVersion
 
 
 The ``fslpy`` version number consists of three numbers, separated by a period,
-which roughly obeys the Semantic Versioning conventions (http://semver.org/):
+roughly obeys the Semantic Versioning conventions (http://semver.org/), and
+is compatible with PEP 440 (https://www.python.org/dev/peps/pep-0440/):
+
 
  1. The major release number. This gets updated for major/external releases.
 
@@ -26,13 +29,19 @@ which roughly obeys the Semantic Versioning conventions (http://semver.org/):
 
  3. The point release number. This gets updated for minor/internal releases,
     which primarily involve bug-fixes and minor changes.
+
+
+The sole exception to the above convention are evelopment versions, which end
+in ``'.dev'``.
 """
 
 
-import string
+import os.path as op
+import            re
+import            string
 
 
-__version__ = 'dev'
+__version__ = '1.0.2.dev'
 """Current version number, as a string. """
 
 
@@ -44,21 +53,26 @@ def parseVersionString(versionString):
     An error is raised if the ``versionString`` is invalid.
     """
 
-    if versionString == 'dev':
-        return 9999, 9999, 9999
-
     components = versionString.split('.')
 
+    # Truncate after three elements -
+    # a development (unreleased0 version
+    # number will end with '.dev', but
+    # we ignore this for the purposes of
+    # comparison.
+    if len(components) == 4 and components[3] == 'dev':
+        components = components[:3]
+
     # Major, minor, and point
     # version are always numeric
     major, minor, point = [c for c in components]
 
-    # Early versions of FSLeyes
+    # But early versions of FSLeyes
     # used a letter at the end
     # to denote a hotfix release.
     # Don't break if we get one
     # of these old version numbers.
-    point = point.strip(string.ascii_letters)
+    point = point.rstrip(string.ascii_letters)
 
     return [int(c) for c in [major, minor, point]]
 
@@ -91,3 +105,27 @@ def compareVersions(v1, v2, ignorePoint=False):
         if p1 < p2: return -1
 
     return 0
+
+
+def patchVersion(filename, newversion):
+    """Patches the given ``filename``, in place, with the given
+    ``newversion``. Searches for a line of the form::
+
+        __version__ = '<oldversion>'
+
+    and replaces ``<oldversion>`` with ``newversion``.
+    """
+    filename = op.abspath(filename)
+
+    with open(filename, 'rt') as f:
+        lines = f.readlines()
+
+    pattern = re.compile('^__version__ *= *\'.*\' *$')
+
+    for i, line in enumerate(lines):
+        if pattern.match(line):
+            lines[i] = '__version__ = \'{0}\'\n'.format(newversion)
+            break
+
+    with open(filename, 'wt') as f:
+        lines = f.writelines(lines)
diff --git a/pytest.ini b/pytest.ini
index 4352354c39baad6c64cc31779521515065481c51..374b89e670b74d8e492393ec619249b1c5ee9cf2 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,3 +1,3 @@
 [pytest]
 testpaths = tests
-addopts   = -s -v --niters=50 --cov=fsl --cov-report=html --html=report.html
+addopts   = -s -v --niters=50 --cov=fsl --cov-report=html --html=report.html --cov-append
diff --git a/requirements.txt b/requirements.txt
index dd852d16ed620cb5d8943efe4fb353c716e49865..55d5ba9466e17d361649fd89f284c5c7c1337986 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
-six>=1.10.0,<2.0
-numpy>=1.11.1,<2.0
-nibabel>=2.1,<3.0
-indexed_gzip>=0.3.3,<0.4
-wxPython>=3.0.2.0,<=4.0.0a2
+six==1.*
+numpy==1.*
+nibabel==2.*
+indexed_gzip==0.3.*
+wxPython>=3.0.2.0,<4.1
diff --git a/setup.py b/setup.py
index 8cb8e1dff62868e3fb34cfdd161c30c6b4db3be3..37848d4f041e74f671c6396cf67e2d8732192b77 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@ with open(op.join(basedir, "fsl", "version.py")) as f:
             break
 version = version['__version__']
 
-with open(op.join(basedir, 'README.md'), 'rt') as f:
+with open(op.join(basedir, 'README.rst'), 'rt') as f:
     readme = f.read()
 
 
@@ -98,7 +98,9 @@ setup(
         'Intended Audience :: Developers',
         'License :: OSI Approved :: Apache Software License',
         'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.4',
         'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
         'Topic :: Software Development :: Libraries :: Python Modules'],
 
     packages=packages,
@@ -106,6 +108,7 @@ setup(
     setup_requires=['pytest-runner', 'sphinx', 'sphinx-rtd-theme', 'mock'],
 
     tests_require=['mock',
+                   'coverage',
                    'pytest-cov',
                    'pytest-html',
                    'pytest-runner',
diff --git a/tests/test_memoize.py b/tests/test_memoize.py
index 4e7142328b287a67eea223b05cd45bcb22bb0e4e..63ce1a267ba5ee00e3551e7ea81bf27b3c618054 100644
--- a/tests/test_memoize.py
+++ b/tests/test_memoize.py
@@ -5,6 +5,7 @@
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
 
+import six
 
 import numpy as np
 
@@ -17,14 +18,13 @@ def test_memoize():
 
     def thefunc(*args, **kwargs):
         timesCalled[0] += 1
-        
+
         if   len(args) + len(kwargs) == 0: return 0
         elif len(args)               == 1: return args[0]         * 5
         else:                              return kwargs['value'] * 5
 
     memoized = memoize.memoize(thefunc)
 
-
     # No args
     for i in range(5):
         assert memoized() == 0
@@ -42,6 +42,13 @@ def test_memoize():
             assert memoized(value=i) == i * 5
         assert timesCalled[0] == 6
 
+    # Unicode arg
+    s = six.u('\u25B2')
+    assert memoized(s) == s * 5
+    assert timesCalled[0] == 7
+    assert memoized(s) == s * 5
+    assert timesCalled[0] == 7
+
 
 def test_memoizeMD5():
     timesCalled = [0]
@@ -50,7 +57,7 @@ def test_memoizeMD5():
         timesCalled[0] += 1
         if   len(args) + len(kwargs) == 0: return 0
         elif len(args)               == 1: return args[0]         * 5
-        else:                              return kwargs['value'] * 5 
+        else:                              return kwargs['value'] * 5
 
     memoized = memoize.memoizeMD5(thefunc)
 
@@ -69,7 +76,14 @@ def test_memoizeMD5():
     for i in range(3):
         for i in range(5):
             assert memoized(value=i) == i * 5
-        assert timesCalled[0] == 6 
+        assert timesCalled[0] == 6
+
+    # Unicode arg (and return value)
+    s = six.u('\u25B2')
+    assert memoized(s) == s * 5
+    assert timesCalled[0] == 7
+    assert memoized(s) == s * 5
+    assert timesCalled[0] == 7
 
 
 def test_skipUnchanged():
@@ -90,66 +104,66 @@ def test_skipUnchanged():
     wrapped('key1', 11)
     wrapped('key2', 12)
     wrapped('key3', 13)
-    
+
     assert timesCalled['key1'] == 1
     assert timesCalled['key2'] == 1
     assert timesCalled['key3'] == 1
-    
+
     wrapped('key1', 11)
     wrapped('key2', 12)
     wrapped('key3', 13)
-    
+
     assert timesCalled['key1'] == 1
     assert timesCalled['key2'] == 1
-    assert timesCalled['key3'] == 1 
+    assert timesCalled['key3'] == 1
 
     wrapped('key1', 14)
     wrapped('key2', 15)
     wrapped('key3', 16)
-    
+
     assert timesCalled['key1'] == 2
     assert timesCalled['key2'] == 2
-    assert timesCalled['key3'] == 2 
- 
+    assert timesCalled['key3'] == 2
+
     wrapped('key1', 14)
     wrapped('key2', 15)
     wrapped('key3', 16)
-    
+
     assert timesCalled['key1'] == 2
     assert timesCalled['key2'] == 2
     assert timesCalled['key3'] == 2
- 
+
     wrapped('key1', 11)
     wrapped('key2', 12)
     wrapped('key3', 13)
-    
+
     assert timesCalled['key1'] == 3
     assert timesCalled['key2'] == 3
-    assert timesCalled['key3'] == 3 
- 
+    assert timesCalled['key3'] == 3
+
     wrapped('key1', np.array([11, 12]))
     wrapped('key2', np.array([13, 14]))
     wrapped('key3', np.array([15, 16]))
-    
+
     assert timesCalled['key1'] == 4
     assert timesCalled['key2'] == 4
-    assert timesCalled['key3'] == 4 
+    assert timesCalled['key3'] == 4
 
     wrapped('key1', np.array([12, 11]))
     wrapped('key2', np.array([14, 13]))
     wrapped('key3', np.array([16, 15]))
-    
+
     assert timesCalled['key1'] == 5
     assert timesCalled['key2'] == 5
-    assert timesCalled['key3'] == 5 
+    assert timesCalled['key3'] == 5
 
     wrapped('key1', np.array([12, 11]))
     wrapped('key2', np.array([14, 13]))
     wrapped('key3', np.array([16, 15]))
-    
+
     assert timesCalled['key1'] == 5
     assert timesCalled['key2'] == 5
-    assert timesCalled['key3'] == 5 
+    assert timesCalled['key3'] == 5
 
 
 def test_Instanceify():
@@ -169,7 +183,7 @@ def test_Instanceify():
         @memoize.Instanceify(memoize.skipUnchanged)
         def setter2(self, name, value):
             self.setter2Called += 1
-        
+
         @memoize.Instanceify(memoize.memoize)
         def func1(self, arg):
             self.func1Called += 1
@@ -196,41 +210,41 @@ def test_Instanceify():
         c1.setter1('blob', 120)
         c1.check(1, 0, 0, 0)
         c2.check(0, 0, 0, 0)
-        
+
     for i in range(3):
         c1.setter1('blob', 150)
         c1.check(2, 0, 0, 0)
-        c2.check(0, 0, 0, 0) 
-        
+        c2.check(0, 0, 0, 0)
+
     for i in range(3):
         c1.setter1('flob', 200)
         c1.check(3, 0, 0, 0)
         c2.check(0, 0, 0, 0)
-        
+
     for i in range(3):
         c1.setter1('flob', 180)
         c1.check(4, 0, 0, 0)
         c2.check(0, 0, 0, 0)
-        
+
     for i in range(3):
         c2.setter1('blob', 120)
         c1.check(4, 0, 0, 0)
         c2.check(1, 0, 0, 0)
-        
+
     for i in range(3):
         c2.setter1('blob', 150)
         c1.check(4, 0, 0, 0)
-        c2.check(2, 0, 0, 0) 
-        
+        c2.check(2, 0, 0, 0)
+
     for i in range(3):
         c2.setter1('flob', 200)
         c1.check(4, 0, 0, 0)
         c2.check(3, 0, 0, 0)
-        
+
     for i in range(3):
         c2.setter1('flob', 180)
         c1.check(4, 0, 0, 0)
-        c2.check(4, 0, 0, 0) 
+        c2.check(4, 0, 0, 0)
 
     # Call setter2 on one instance,
     # ...
@@ -238,63 +252,63 @@ def test_Instanceify():
         c1.setter2('blob', 120)
         c1.check(4, 1, 0, 0)
         c2.check(4, 0, 0, 0)
-        
+
     for i in range(3):
         c1.setter2('blob', 150)
         c1.check(4, 2, 0, 0)
-        c2.check(4, 0, 0, 0) 
-        
+        c2.check(4, 0, 0, 0)
+
     for i in range(3):
         c1.setter2('flob', 200)
         c1.check(4, 3, 0, 0)
         c2.check(4, 0, 0, 0)
-        
+
     for i in range(3):
         c1.setter2('flob', 180)
         c1.check(4, 4, 0, 0)
         c2.check(4, 0, 0, 0)
-        
+
     for i in range(3):
         c2.setter2('blob', 120)
         c1.check(4, 4, 0, 0)
         c2.check(4, 1, 0, 0)
-        
+
     for i in range(3):
         c2.setter2('blob', 150)
         c1.check(4, 4, 0, 0)
-        c2.check(4, 2, 0, 0) 
-        
+        c2.check(4, 2, 0, 0)
+
     for i in range(3):
         c2.setter2('flob', 200)
         c1.check(4, 4, 0, 0)
         c2.check(4, 3, 0, 0)
-        
+
     for i in range(3):
         c2.setter2('flob', 180)
         c1.check(4, 4, 0, 0)
-        c2.check(4, 4, 0, 0) 
-         
+        c2.check(4, 4, 0, 0)
+
     # Call func1 on one instance,
     # ...
     for i in range(3):
         assert c1.func1(123) == 246
         c1.check(4, 4, 1, 0)
         c2.check(4, 4, 0, 0)
-        
+
     for i in range(3):
         assert c1.func1(456) == 912
         c1.check(4, 4, 2, 0)
-        c2.check(4, 4, 0, 0) 
-        
+        c2.check(4, 4, 0, 0)
+
     for i in range(3):
         assert c2.func1(123) == 246
         c1.check(4, 4, 2, 0)
         c2.check(4, 4, 1, 0)
-        
+
     for i in range(3):
         assert c2.func1(456) == 912
         c1.check(4, 4, 2, 0)
-        c2.check(4, 4, 2, 0) 
+        c2.check(4, 4, 2, 0)
 
     # Call func2 on one instance,
     # ...
@@ -302,16 +316,16 @@ def test_Instanceify():
         assert c1.func2(123) == 492
         c1.check(4, 4, 2, 1)
         c2.check(4, 4, 2, 0)
-        
+
     for i in range(3):
         assert c1.func2(456) == 1824
-        
+
     for i in range(3):
         assert c2.func2(123) == 492
         c1.check(4, 4, 2, 2)
         c2.check(4, 4, 2, 1)
-        
+
     for i in range(3):
         assert c2.func2(456) == 1824
         c1.check(4, 4, 2, 2)
-        c2.check(4, 4, 2, 2) 
+        c2.check(4, 4, 2, 2)