Ken Watford avatar Ken Watford committed 7567cdd

metis 0.1a

Comments (0)

Files changed (7)

+syntax: glob
+*~
+*.pyc
+*.egg-info
+.DS_Store
+*.class
+dist/
+build/
+MANIFEST
+docs/_build
+Copyright (c) 2012 Ken Watford
+
+Permission is hereby granted, free of charge, to any person 
+obtaining a copy of this software and associated documentation 
+files (the "Software"), to deal in the Software without 
+restriction, including without limitation the rights to use, 
+copy, modify, merge, publish, distribute, sublicense, and/or 
+sell copies of the Software, and to permit persons to whom the 
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be 
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+tl;dr - MIT license.
+include *.txt
+METIS for Python
+================
+
+Wrapper for the METIS library for partitioning graphs (and other stuff).
+
+This library is unrelated to PyMetis, except that they wrap the same library.
+PyMetis is a Boost Python extension, while this library is pure python and will
+run under PyPy and interpreters with similarly compatible ctypes libraries.
+
+The functions of primary interest are:
+
+    `metis.part_graph_kway`
+    `metis.part_graph_recur`
+
+They are also the only objects export by "from metis import *".
+Other objects in the module may be of interest to those looking to 
+mangle their graph datastructures into the required format. Examples
+of this include the `networkx_to_metis` and `adjlist_to_metis` functions.
+These are automatically called by the `part_graph_` functions, so there is
+little need to call them manually.
+
+See the repository_ for updates and issue reporting.
+
+Installation
+============
+
+It's on PyPI, so installation should be as easy as::
+
+    pip install metis
+          -or-
+    easy_install metis
+
+METIS itself is not included with this wrapper. Get it here_.
+
+Note that the shared library is needed, and isn't enabled by default
+by the configuration process. Turn it on by issuing:
+    
+    make config shared=1
+
+Your operating system's package manager might know about METIS,
+but this wrapper was designed for use with METIS 5. Packages with
+METIS 4 will probably not work.
+
+This wrapper uses Python's ctypes module to interface with the METIS
+shared library. If it is unable to automatically locate the library, you
+may specify the full path to the library file in the METIS_DLL environment
+variable.
+
+Additionally, there are a few compile options that you may need to tell
+the wrapper about. The sizes of the "idx_t" and "real_t" types are not
+easily determinable at runtime, so they can be provided with the 
+environment variables METIS_IDXTYPEWIDTH and METIS_REALTYPEWIDTH.
+The default value for each of these (at both compile and in this library)
+is 32, but they may be set to 64 if desired. If the values do not match
+what was used to compile the library, Bad Things™ will occur.
+
+.. _here: http://glaros.dtc.umn.edu/gkhome/views/metis
+.. _repository: http://bitbucket.org/kw/metis-python

distribute_setup.py

+#!python
+"""Bootstrap distribute installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from distribute_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import os
+import sys
+import time
+import fnmatch
+import tempfile
+import tarfile
+from distutils import log
+
+try:
+    from site import USER_SITE
+except ImportError:
+    USER_SITE = None
+
+try:
+    import subprocess
+
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        return subprocess.call(args) == 0
+
+except ImportError:
+    # will be used for python 2.3
+    def _python_cmd(*args):
+        args = (sys.executable,) + args
+        # quoting arguments if windows
+        if sys.platform == 'win32':
+            def quote(arg):
+                if ' ' in arg:
+                    return '"%s"' % arg
+                return arg
+            args = [quote(arg) for arg in args]
+        return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
+
+DEFAULT_VERSION = "0.6.19"
+DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
+SETUPTOOLS_FAKED_VERSION = "0.6c11"
+
+SETUPTOOLS_PKG_INFO = """\
+Metadata-Version: 1.0
+Name: setuptools
+Version: %s
+Summary: xxxx
+Home-page: xxx
+Author: xxx
+Author-email: xxx
+License: xxx
+Description: xxx
+""" % SETUPTOOLS_FAKED_VERSION
+
+
+def _install(tarball):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # installing
+        log.warn('Installing Distribute')
+        if not _python_cmd('setup.py', 'install'):
+            log.warn('Something went wrong during the installation.')
+            log.warn('See the error message above.')
+    finally:
+        os.chdir(old_wd)
+
+
+def _build_egg(egg, tarball, to_dir):
+    # extracting the tarball
+    tmpdir = tempfile.mkdtemp()
+    log.warn('Extracting in %s', tmpdir)
+    old_wd = os.getcwd()
+    try:
+        os.chdir(tmpdir)
+        tar = tarfile.open(tarball)
+        _extractall(tar)
+        tar.close()
+
+        # going in the directory
+        subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
+        os.chdir(subdir)
+        log.warn('Now working in %s', subdir)
+
+        # building an egg
+        log.warn('Building a Distribute egg in %s', to_dir)
+        _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
+
+    finally:
+        os.chdir(old_wd)
+    # returning the result
+    log.warn(egg)
+    if not os.path.exists(egg):
+        raise IOError('Could not build the egg.')
+
+
+def _do_download(version, download_base, to_dir, download_delay):
+    egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg'
+                       % (version, sys.version_info[0], sys.version_info[1]))
+    if not os.path.exists(egg):
+        tarball = download_setuptools(version, download_base,
+                                      to_dir, download_delay)
+        _build_egg(egg, tarball, to_dir)
+    sys.path.insert(0, egg)
+    import setuptools
+    setuptools.bootstrap_install_from = egg
+
+
+def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                   to_dir=os.curdir, download_delay=15, no_fake=True):
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    was_imported = 'pkg_resources' in sys.modules or \
+        'setuptools' in sys.modules
+    try:
+        try:
+            import pkg_resources
+            if not hasattr(pkg_resources, '_distribute'):
+                if not no_fake:
+                    _fake_setuptools()
+                raise ImportError
+        except ImportError:
+            return _do_download(version, download_base, to_dir, download_delay)
+        try:
+            pkg_resources.require("distribute>="+version)
+            return
+        except pkg_resources.VersionConflict:
+            e = sys.exc_info()[1]
+            if was_imported:
+                sys.stderr.write(
+                "The required version of distribute (>=%s) is not available,\n"
+                "and can't be installed while this script is running. Please\n"
+                "install a more recent version first, using\n"
+                "'easy_install -U distribute'."
+                "\n\n(Currently using %r)\n" % (version, e.args[0]))
+                sys.exit(2)
+            else:
+                del pkg_resources, sys.modules['pkg_resources']    # reload ok
+                return _do_download(version, download_base, to_dir,
+                                    download_delay)
+        except pkg_resources.DistributionNotFound:
+            return _do_download(version, download_base, to_dir,
+                                download_delay)
+    finally:
+        if not no_fake:
+            _create_fake_setuptools_pkg_info(to_dir)
+
+def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
+                        to_dir=os.curdir, delay=15):
+    """Download distribute from a specified location and return its filename
+
+    `version` should be a valid distribute version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download
+    attempt.
+    """
+    # making sure we use the absolute path
+    to_dir = os.path.abspath(to_dir)
+    try:
+        from urllib.request import urlopen
+    except ImportError:
+        from urllib2 import urlopen
+    tgz_name = "distribute-%s.tar.gz" % version
+    url = download_base + tgz_name
+    saveto = os.path.join(to_dir, tgz_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            log.warn("Downloading %s", url)
+            src = urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = src.read()
+            dst = open(saveto, "wb")
+            dst.write(data)
+        finally:
+            if src:
+                src.close()
+            if dst:
+                dst.close()
+    return os.path.realpath(saveto)
+
+def _no_sandbox(function):
+    def __no_sandbox(*args, **kw):
+        try:
+            from setuptools.sandbox import DirectorySandbox
+            if not hasattr(DirectorySandbox, '_old'):
+                def violation(*args):
+                    pass
+                DirectorySandbox._old = DirectorySandbox._violation
+                DirectorySandbox._violation = violation
+                patched = True
+            else:
+                patched = False
+        except ImportError:
+            patched = False
+
+        try:
+            return function(*args, **kw)
+        finally:
+            if patched:
+                DirectorySandbox._violation = DirectorySandbox._old
+                del DirectorySandbox._old
+
+    return __no_sandbox
+
+def _patch_file(path, content):
+    """Will backup the file then patch it"""
+    existing_content = open(path).read()
+    if existing_content == content:
+        # already patched
+        log.warn('Already patched.')
+        return False
+    log.warn('Patching...')
+    _rename_path(path)
+    f = open(path, 'w')
+    try:
+        f.write(content)
+    finally:
+        f.close()
+    return True
+
+_patch_file = _no_sandbox(_patch_file)
+
+def _same_content(path, content):
+    return open(path).read() == content
+
+def _rename_path(path):
+    new_name = path + '.OLD.%s' % time.time()
+    log.warn('Renaming %s into %s', path, new_name)
+    os.rename(path, new_name)
+    return new_name
+
+def _remove_flat_installation(placeholder):
+    if not os.path.isdir(placeholder):
+        log.warn('Unkown installation at %s', placeholder)
+        return False
+    found = False
+    for file in os.listdir(placeholder):
+        if fnmatch.fnmatch(file, 'setuptools*.egg-info'):
+            found = True
+            break
+    if not found:
+        log.warn('Could not locate setuptools*.egg-info')
+        return
+
+    log.warn('Removing elements out of the way...')
+    pkg_info = os.path.join(placeholder, file)
+    if os.path.isdir(pkg_info):
+        patched = _patch_egg_dir(pkg_info)
+    else:
+        patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO)
+
+    if not patched:
+        log.warn('%s already patched.', pkg_info)
+        return False
+    # now let's move the files out of the way
+    for element in ('setuptools', 'pkg_resources.py', 'site.py'):
+        element = os.path.join(placeholder, element)
+        if os.path.exists(element):
+            _rename_path(element)
+        else:
+            log.warn('Could not find the %s element of the '
+                     'Setuptools distribution', element)
+    return True
+
+_remove_flat_installation = _no_sandbox(_remove_flat_installation)
+
+def _after_install(dist):
+    log.warn('After install bootstrap.')
+    placeholder = dist.get_command_obj('install').install_purelib
+    _create_fake_setuptools_pkg_info(placeholder)
+
+def _create_fake_setuptools_pkg_info(placeholder):
+    if not placeholder or not os.path.exists(placeholder):
+        log.warn('Could not find the install location')
+        return
+    pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1])
+    setuptools_file = 'setuptools-%s-py%s.egg-info' % \
+            (SETUPTOOLS_FAKED_VERSION, pyver)
+    pkg_info = os.path.join(placeholder, setuptools_file)
+    if os.path.exists(pkg_info):
+        log.warn('%s already exists', pkg_info)
+        return
+
+    log.warn('Creating %s', pkg_info)
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+
+    pth_file = os.path.join(placeholder, 'setuptools.pth')
+    log.warn('Creating %s', pth_file)
+    f = open(pth_file, 'w')
+    try:
+        f.write(os.path.join(os.curdir, setuptools_file))
+    finally:
+        f.close()
+
+_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info)
+
+def _patch_egg_dir(path):
+    # let's check if it's already patched
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    if os.path.exists(pkg_info):
+        if _same_content(pkg_info, SETUPTOOLS_PKG_INFO):
+            log.warn('%s already patched.', pkg_info)
+            return False
+    _rename_path(path)
+    os.mkdir(path)
+    os.mkdir(os.path.join(path, 'EGG-INFO'))
+    pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO')
+    f = open(pkg_info, 'w')
+    try:
+        f.write(SETUPTOOLS_PKG_INFO)
+    finally:
+        f.close()
+    return True
+
+_patch_egg_dir = _no_sandbox(_patch_egg_dir)
+
+def _before_install():
+    log.warn('Before install bootstrap.')
+    _fake_setuptools()
+
+
+def _under_prefix(location):
+    if 'install' not in sys.argv:
+        return True
+    args = sys.argv[sys.argv.index('install')+1:]
+    for index, arg in enumerate(args):
+        for option in ('--root', '--prefix'):
+            if arg.startswith('%s=' % option):
+                top_dir = arg.split('root=')[-1]
+                return location.startswith(top_dir)
+            elif arg == option:
+                if len(args) > index:
+                    top_dir = args[index+1]
+                    return location.startswith(top_dir)
+        if arg == '--user' and USER_SITE is not None:
+            return location.startswith(USER_SITE)
+    return True
+
+
+def _fake_setuptools():
+    log.warn('Scanning installed packages')
+    try:
+        import pkg_resources
+    except ImportError:
+        # we're cool
+        log.warn('Setuptools or Distribute does not seem to be installed.')
+        return
+    ws = pkg_resources.working_set
+    try:
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools',
+                                  replacement=False))
+    except TypeError:
+        # old distribute API
+        setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools'))
+
+    if setuptools_dist is None:
+        log.warn('No setuptools distribution found')
+        return
+    # detecting if it was already faked
+    setuptools_location = setuptools_dist.location
+    log.warn('Setuptools installation detected at %s', setuptools_location)
+
+    # if --root or --preix was provided, and if
+    # setuptools is not located in them, we don't patch it
+    if not _under_prefix(setuptools_location):
+        log.warn('Not patching, --root or --prefix is installing Distribute'
+                 ' in another location')
+        return
+
+    # let's see if its an egg
+    if not setuptools_location.endswith('.egg'):
+        log.warn('Non-egg installation')
+        res = _remove_flat_installation(setuptools_location)
+        if not res:
+            return
+    else:
+        log.warn('Egg installation')
+        pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO')
+        if (os.path.exists(pkg_info) and
+            _same_content(pkg_info, SETUPTOOLS_PKG_INFO)):
+            log.warn('Already patched.')
+            return
+        log.warn('Patching...')
+        # let's create a fake egg replacing setuptools one
+        res = _patch_egg_dir(setuptools_location)
+        if not res:
+            return
+    log.warn('Patched done.')
+    _relaunch()
+
+
+def _relaunch():
+    log.warn('Relaunching...')
+    # we have to relaunch the process
+    # pip marker to avoid a relaunch bug
+    if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']:
+        sys.argv[0] = 'setup.py'
+    args = [sys.executable] + sys.argv
+    sys.exit(subprocess.call(args))
+
+
+def _extractall(self, path=".", members=None):
+    """Extract all members from the archive to the current working
+       directory and set owner, modification time and permissions on
+       directories afterwards. `path' specifies a different directory
+       to extract to. `members' is optional and must be a subset of the
+       list returned by getmembers().
+    """
+    import copy
+    import operator
+    from tarfile import ExtractError
+    directories = []
+
+    if members is None:
+        members = self
+
+    for tarinfo in members:
+        if tarinfo.isdir():
+            # Extract directories with a safe mode.
+            directories.append(tarinfo)
+            tarinfo = copy.copy(tarinfo)
+            tarinfo.mode = 448 # decimal for oct 0700
+        self.extract(tarinfo, path)
+
+    # Reverse sort directories.
+    if sys.version_info < (2, 4):
+        def sorter(dir1, dir2):
+            return cmp(dir1.name, dir2.name)
+        directories.sort(sorter)
+        directories.reverse()
+    else:
+        directories.sort(key=operator.attrgetter('name'), reverse=True)
+
+    # Set correct owner, mtime and filemode on directories.
+    for tarinfo in directories:
+        dirpath = os.path.join(path, tarinfo.name)
+        try:
+            self.chown(tarinfo, dirpath)
+            self.utime(tarinfo, dirpath)
+            self.chmod(tarinfo, dirpath)
+        except ExtractError:
+            e = sys.exc_info()[1]
+            if self.errorlevel > 1:
+                raise
+            else:
+                self._dbg(1, "tarfile: %s" % e)
+
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+    tarball = download_setuptools()
+    _install(tarball)
+
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
+"""
+Wrapper for the METIS library for partitioning graphs (and other stuff).
+
+This library is unrelated to PyMetis, except that they wrap the same library.
+PyMetis is a Boost Python extension, while this library is pure python and will
+run under PyPy and interpreters with similarly compatible ctypes libraries.
+
+METIS itself is not included with this wrapper. Get it here:
+   http://glaros.dtc.umn.edu/gkhome/views/metis
+
+Note that the shared library is needed, and isn't enabled by default
+by the configuration process. Turn it on by issuing:
+    
+    make config shared=1
+
+Your operating system's package manager might know about METIS,
+but this wrapper was designed for use with METIS 5. Packages with
+METIS 4 will not work.
+
+This wrapper uses Python's ctypes module to interface with the METIS
+shared library. If it is unable to automatically locate the library, you
+may specify the full path to the library file in the METIS_DLL environment
+variable.
+
+Additionally, there are a few compile options that you may need to tell
+the wrapper about. The sizes of the "idx_t" and "real_t" types are not
+easily determinable at runtime, so they can be provided with the 
+environment variables METIS_IDXTYPEWIDTH and METIS_REALTYPEWIDTH.
+The default value for each of these (at both compile and in this library)
+is 32, but they may be set to 64 if desired. If the values do not match
+what was used to compile the library, Bad Things(TM) will occur.
+
+The functions of primary interest in this module are:
+
+    `metis.part_graph_kway`
+    `metis.part_graph_recur`
+
+They are also the only objects export by "from metis import *".
+Other objects in the module may be of interest to those looking to 
+mangle their graph datastructures into the required format. Examples
+of this include the `networkx_to_metis` and `adjlist_to_metis` functions.
+These are automatically called by the `part_graph_` functions, so there is
+little need to call them manually.
+
+See http://bitbucket.org/kw/metis-python for updates and issue reporting.
+"""
+
+__version__ = '0.1a'
+
+import ctypes
+from ctypes import POINTER as P, byref
+import os, sys, operator as op
+from warnings import warn
+from collections import namedtuple
+try:
+    import networkx
+except ImportError:
+    networkx = None
+
+__all__ = ['part_graph_kway', 'part_graph_recur']
+
+# Sadly, METIS does not currently include any API call to determine
+# the correct datatypes. So we either have to guess, let the user tell
+# us, try to infer it by checking API behavior on test inputs, or
+# look for the header and parse out the preprocessor macros.
+# Since we're in a bit of a hurry, for now we'll use the defaults
+# and let the user specify if this is wrong with env vars.
+IDXTYPEWIDTH  = os.getenv('METIS_IDXTYPEWIDTH', '32')
+REALTYPEWIDTH = os.getenv('METIS_REALTYPEWIDTH', '32')
+
+if IDXTYPEWIDTH == '32':
+    idx_t = ctypes.c_int32
+elif IDXTYPEWIDTH == '64':
+    idx_t = ctypes.c_int64
+else:
+    raise EnvironmentError('Env var METIS_IDXTYPEWIDTH must be "32" or "64"')
+
+if REALTYPEWIDTH == '32':
+    real_t = ctypes.c_float
+elif REALTYPEWIDTH == '64':
+    real_t = ctypes.c_double
+else:
+    raise EnvironmentError('Env var METIS_REALTYPEWIDTH must be "32" or "64"')
+
+
+METIS_NOPTIONS = 40
+
+# The _enum and _bitfield base classes come from my PyCL project
+# They make enum constants a little more friendly.
+class _enum(ctypes.c_int32):
+    # Base class for various enums
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return False
+        else:
+            return self.value == other.value
+    def __ne__(self, other):
+        return not(self == other)
+    def __hash__(self):
+        return self.value.__hash__()
+    def __repr__(self):
+        by_value = self.__class__._by_value
+        names = []
+        if self in by_value:
+            return by_value[self]
+        else:
+            return "UNKNOWN(0x%x)" % self.value
+    def shortname(self):        
+        return self._by_value_short.get(self, 'unknown')
+    @classmethod
+    def fromname(cls, name):
+        if name in cls._by_name:
+            return cls._by_name[name]
+        elif name in cls._by_name_short:
+            return cls._by_name_short[name]
+        else:
+            raise KeyError('Unknown name: %s' % name) 
+    @classmethod
+    def toname(cls, val):
+        if val in self._by_value_short:
+            return cls._by_value_short[value]
+        elif val in cls._by_value:
+            return cls._by_value[value]
+        else:
+            raise KeyError
+
+class _bitfield(ctypes.c_int32):
+    # Base class for bitfield values
+    # Bitwise operations for combining flags are supported.
+    def __or__(self, other):
+        assert isinstance(other, self.__class__)
+        return self.__class__(self.value | other.value)
+    def __and__(self, other):
+        assert isinstance(other, self.__class__)
+        return self.__class__(self.value & other.value)
+    def __xor__(self):
+        assert isinstance(other, self.__class__)
+        return self.__class__(self.value ^ other.value)
+    def __not__(self):
+        return self.__class__(~self.value)
+    def __contains__(self, other):
+        assert isinstance(other, self.__class__)
+        return (self.value & other.value) == other.value
+    def __hash__(self):
+        return self.value.__hash__()
+    def __eq__(self, other):
+        if not isinstance(other, self.__class__):
+            return False
+        else:
+            return self.value == other.value
+    def __ne__(self, other):
+        return not(self == other)
+    def __repr__(self):
+        by_value = self.__class__._by_value
+        names = []
+        if self in by_value:
+            return by_value[self]
+        for val in by_value:
+            if val in self:
+                names.append(by_value[val])
+        if names:
+            return " | ".join(names)
+        else:
+            return "UNKNOWN(0x%x)" % self.value
+
+class rstatus_et(_enum):
+    METIS_OK              =  1    #/*!< Returned normally */
+    METIS_ERROR_INPUT     = -2   #/*!< Returned due to erroneous inputs and/or options */
+    METIS_ERROR_MEMORY    = -3   #/*!< Returned due to insufficient memory */
+    METIS_ERROR           = -4   #/*!< Some other errors */
+
+class moptype_et(_enum):
+    METIS_OP_PMETIS = 0
+    METIS_OP_KMETIS = 1
+    METIS_OP_OMETIS = 2
+
+class moptions_et(_enum):
+    METIS_OPTION_PTYPE     =  0
+    METIS_OPTION_OBJTYPE   =  1
+    METIS_OPTION_CTYPE     =  2
+    METIS_OPTION_IPTYPE    =  3
+    METIS_OPTION_RTYPE     =  4
+    METIS_OPTION_DBGLVL    =  5
+    METIS_OPTION_NITER     =  6
+    METIS_OPTION_NCUTS     =  7
+    METIS_OPTION_SEED      =  8
+    METIS_OPTION_MINCONN   =  9 
+    METIS_OPTION_CONTIG    = 10
+    METIS_OPTION_COMPRESS  = 11
+    METIS_OPTION_CCORDER   = 12 
+    METIS_OPTION_PFACTOR   = 13
+    METIS_OPTION_NSEPS     = 14
+    METIS_OPTION_UFACTOR   = 15
+    METIS_OPTION_NUMBERING = 16
+    #/* Used for command-line parameter purposes */
+    METIS_OPTION_HELP      = 17
+    METIS_OPTION_TPWGTS    = 18 
+    METIS_OPTION_NCOMMON   = 19
+    METIS_OPTION_NOOUTPUT  = 20
+    METIS_OPTION_BALANCE   = 21 
+    METIS_OPTION_GTYPE     = 22
+    METIS_OPTION_UBVEC     = 23
+
+class mptype_et(_enum):
+    METIS_PTYPE_DEFAULT = -1
+    METIS_PTYPE_RB   = 0
+    METIS_PTYPE_KWAY = 1              
+
+class mgtype_et(_enum):
+    METIS_GTYPE_DEFAULT = -1
+    METIS_GTYPE_DUAL  = 0
+    METIS_GTYPE_NODAL = 1
+
+class mctype_et(_enum):
+    METIS_CTYPE_DEFAULT = -1
+    METIS_CTYPE_RM   = 0
+    METIS_CTYPE_SHEM = 1
+
+class miptype_et(_enum):
+    METIS_IPTYPE_DEFAULT = -1
+    METIS_IPTYPE_GROW    = 0
+    METIS_IPTYPE_RANDOM  = 1
+    METIS_IPTYPE_EDGE    = 2
+    METIS_IPTYPE_NODE    = 3
+    METIS_IPTYPE_METISRB = 4
+
+class mrtype_et(_enum):
+    METIS_RTYPE_DEFAULT   = -1
+    METIS_RTYPE_FM        = 0
+    METIS_RTYPE_GREEDY    = 1
+    METIS_RTYPE_SEP2SIDED = 2
+    METIS_RTYPE_SEP1SIDED = 3
+
+class mdbglvl_et(_bitfield):
+    METIS_DBG_DEFAULT    = -1
+    METIS_DBG_INFO       = 1       #/*!< Shows various diagnostic messages */
+    METIS_DBG_TIME       = 2       #/*!< Perform timing analysis */
+    METIS_DBG_COARSEN    = 4       #/*!< Show the coarsening progress */
+    METIS_DBG_REFINE     = 8       #/*!< Show the refinement progress */
+    METIS_DBG_IPART      = 16      #/*!< Show info on initial partitioning */
+    METIS_DBG_MOVEINFO   = 32      #/*!< Show info on vertex moves during refinement */
+    METIS_DBG_SEPINFO    = 64      #/*!< Show info on vertex moves during sep refinement */
+    METIS_DBG_CONNINFO   = 128     #/*!< Show info on minimization of subdomain connectivity */
+    METIS_DBG_CONTIGINFO = 256     #/*!< Show info on elimination of connected components */ 
+    METIS_DBG_MEMORY     = 2048    #/*!< Show info related to wspace allocation */
+
+class mobjtype_et(_enum):
+    METIS_OBJTYPE_DEFAULT = -1
+    METIS_OBJTYPE_CUT  = 0
+    METIS_OBJTYPE_VOL  = 1
+    METIS_OBJTYPE_NODE = 2
+
+# For enums and bitfields, do magic. Each type gets a registry of the
+# names and values of their defined elements, to support pretty printing.
+# Further, each of the class variables (which are defined using ints) is
+# upgraded to be a member of the class in question.
+# Additionally, each of the constants is copied into the module scope.
+for cls in (_enum.__subclasses__() + _bitfield.__subclasses__()):
+    if cls.__name__ not in globals() or cls.__name__.startswith('_'):
+        # Don't apply this to types that ctypes makes automatically,
+        # like the _be classes. Doing so will overwrite the declared
+        # constants at global scope, which is really weird.
+        continue
+    cls._by_name = dict()
+    cls._by_value = dict()
+    cls._by_name_short = dict()
+    cls._by_value_short = dict()
+    if not cls.__doc__:
+        cls.__doc__ = ""
+    for name, value in cls.__dict__.items():
+        if isinstance(value, int):
+            obj = cls(value)
+            setattr(cls, name, obj)
+            cls._by_name[name] = obj
+            cls._by_value[obj] = name
+            shortname = name.split('_')[-1].lower()
+            cls._by_name_short[shortname] = obj
+            cls._by_value_short[obj] = shortname
+            globals()[name] = obj
+            cls.__doc__ += """
+            .. attribute:: %s
+            """ % name
+# cleanup
+del cls; del name; del value; del obj
+
+
+# Convert values taken from option array into appropriate enum
+_opt_types = {
+    METIS_OPTION_PTYPE   : mptype_et,
+    METIS_OPTION_OBJTYPE : mobjtype_et,
+    METIS_OPTION_CTYPE   : mctype_et,
+    METIS_OPTION_GTYPE   : mgtype_et,
+    METIS_OPTION_IPTYPE  : miptype_et,
+    METIS_OPTION_RTYPE   : mrtype_et,
+    METIS_OPTION_DBGLVL  : mdbglvl_et,
+    }
+
+class METIS_Options(object):
+    """ Represents the 'options' array used to represent all 
+    nearly all options that can be given to METIS functions.
+    Will be used when extra keyword arguments are are used in wrappers.
+
+    Note that I spent way too much time on this. 
+
+    """
+    def __init__(self, options=None, **opts):
+        self.array = (idx_t*METIS_NOPTIONS)()
+        _METIS_SetDefaultOptions(self.array)
+        if options:
+            for opt, val in options.keys():
+                self[opt] = val
+        for opt, val in opts.items():
+            self[opt] = val
+
+    def keys(self):
+        return moptions_et._by_name_short.keys()
+
+    def __getitem__(self, opt):        
+        if isinstance(opt, basestring):
+            opt = moptions_et.fromname(opt)
+        val = self.array[opt.value]   
+        if opt in _opt_types:
+            val = _opt_types[opt](val)
+            if isinstance(val, _enum):
+                val = val.shortname()
+        return val
+
+    def __setitem__(self, opt, val):
+        if isinstance(opt, basestring):
+            opt = moptions_et.fromname(opt)
+        if isinstance(val, basestring) and opt in _opt_types:
+            val = _opt_types[opt].fromname(val)
+        try:
+            self.array[opt.value] = val
+        except TypeError:
+            raise TypeError("Bad type for option %s: %s" %
+                (opt, val.__class__.__name__))
+
+    def __repr__(self):
+        """ Only show non-default options """
+        nondefaults = []
+        for opt in self.keys():
+            realind = moptions_et.fromname(opt).value
+            if self.array[realind] != -1:
+                val = self[opt]
+                nondefaults.append('%s=%r' % (opt, val))
+        return 'METIS_Options(' + ', '.join(nondefaults) + ')'
+   
+
+
+# Attempt to locate and load the appropriate shared library
+_dll_filename = os.getenv('METIS_DLL')
+if not _dll_filename:
+    try:
+        from ctypes.util import find_library as _find_library
+        _dll_filename = _find_library('metis')
+    except ImportError:
+        pass
+if _dll_filename:
+    try:
+        _dll = ctypes.cdll.LoadLibrary(_dll_filename)
+    except:        
+        raise RuntimeError('Could not load METIS dll: %s' % _dll_filename)
+else:
+    if os.environ.get('READTHEDOCS', None) == 'True':
+        # Don't care if we can load the DLL on RTD.
+        _dll = None
+    else:
+        raise RuntimeError('Could not locate METIS dll. Please set the METIS_DLL environment variable to its full path.')
+
+# Wrapping conveniences
+
+def _wrapdll(*argtypes, **kw):
+    """
+    Decorator used to simplify wrapping METIS functions a bit.
+
+    The positional arguments represent the ctypes argument types the
+    C-level function expects, and will be used to do argument type checking.
+
+    If a `res` keyword argument is given, it represents the C-level
+    function's expected return type. The default is `rstatus_et`
+    
+    If an `err` keyword argument is given, it represents an error checker
+    that should be run after low-level calls. The `_result_errcheck` and
+    `_lastarg_errcheck` functions should be sufficient for most OpenCL
+    functions. `_result_errcheck` is the default value.
+
+    The decorated function should have the same name as the underlying
+    METIS function, since the function name is used to do the lookup. The
+    C-level function pointer will be stored in the decorated function's
+    `call` attribute, and should be used by the decorated function to
+    perform the actual call(s). The wrapped function is otherwise untouched.
+
+    """
+    def dowrap(f):
+        if f.__name__.startswith('_'):
+            name = f.__name__[1:]
+        else:
+            name = f.__name__
+        wrapped_func = getattr(_dll, name)        
+        wrapped_func.argtypes = argtypes
+        res = kw.pop('res', rstatus_et)
+        wrapped_func.restype = res
+        err = kw.pop('err', _result_errcheck)
+        wrapped_func.errcheck = err
+        f.call = wrapped_func
+        return f
+    return dowrap
+
+# Translate METIS status messages into Python exceptions
+class METIS_Error(Exception): pass
+class METIS_MemoryError(METIS_Error, MemoryError): pass
+class METIS_InputError(METIS_Error, ValueError): pass
+class METIS_OtherError(METIS_Error): pass
+
+def _result_errcheck(result, func, args):
+    """
+    For use in the errcheck attribute of a ctypes function wrapper.
+
+    Most METIS functions return rstatus_et. This checks it for
+    an error code and raises an appropriate exception if it finds one.
+
+    This is the default error checker when using _wrapdll
+    """
+    if result != METIS_OK:
+        if error == METIS_ERROR_INPUT: raise METIS_InputError
+        if error == METIS_ERROR_MEMORY: raise METIS_MemoryError
+        if error == METIS_ERROR: raise METIS_OtherError
+        raise RuntimeError("Error raising error: Bad error.") # lolwut
+    return result
+
+# Graph helpers
+
+METIS_Graph = namedtuple('METIS_Graph',
+    'nvtxs ncon xadj adjncy vwgt vsize adjwgt')
+
+def networkx_to_metis(G):
+    """ Convert NetworkX graph into something METIS can consume
+    The graph may specify weights and sizes using the following
+    graph attributes:
+
+      edge_weight_attr
+      node_weight_attr
+      node_size_attr
+
+    If node_weight_attr is a list instead of a string, then multiple
+    node weight labels can be provided. 
+
+    All weights must be integer values. If a attr label is specified but 
+    a node/edge is missing that attribute, it defaults to 1.
+    """
+    n = G.number_of_nodes()
+    m = G.number_of_edges()
+    nvtxs = idx_t(n)
+    
+    H = networkx.convert_node_labels_to_integers(G)
+    xadj = (idx_t*(n+1))()
+    adjncy = (idx_t*(2*m))()
+
+    # Check graph attributes for weight/size labels
+    edgew = G.graph.get('edge_weight_attr', None)
+    nodew = G.graph.get('node_weight_attr', [])
+    nodesz = G.graph.get('node_size_attr', None)
+
+    if edgew:
+        adjwgt = (idx_t*(2*m))()
+    else:
+        adjwgt = None
+
+    if nodew:
+        if isinstance(nodew, basestring):
+            nodew = [nodew]            
+        nc = len(nodew)
+        ncon = idx_t(nc)
+        vwgt = (idx_t*(n*len(nodew)))()                
+    else:
+        ncon = idx_t(1)
+        vwgt = None
+
+    if nodesz:
+        vsize = (idx_t*n)()
+    else:
+        vsize = None
+
+    # Fill in each array
+    xadj[0] = e = 0
+    for i in H.node:
+        for c,w in enumerate(nodew):
+            try:            
+                vwgt[i*nc+c] = H.node[i].get(w, 1)
+            except TypeError:
+                raise TypeError("Node weights must be integers" )
+
+        if nodesz:
+            try:
+                vsize[i] = H.node[i].get(nodesz, 1)
+            except TypeError:
+                raise TypeError("Node sizes must be integers")
+
+        for j, attr in H.edge[i].iteritems():
+            adjncy[e] = j            
+            if edgew:
+                try:
+                    adjwgt[e] = attr.get(edgew, 1)
+                except TypeError:
+                    raise TypeError("Edge weights must be integers")
+            e += 1
+        xadj[i+1] = e
+    
+    return METIS_Graph(nvtxs, ncon, xadj, adjncy, vwgt, vsize, adjwgt)
+
+def adjlist_to_metis(adjlist, nodew=None, nodesz=None):
+    """
+    Rudimentary adjacency list converter.
+    Primarily of use if you don't have or don't want to use NetworkX.
+
+    `adjlist` is a list of tuples. Each list element represents a node or vertex
+    in the graph. Each item in the tuples represents an edge. These items may be 
+    single integers representing neighbor index, or they may be an (index, weight)
+    tuple if you want weighted edges. Default weight is 1 for missing weights.
+
+    The graph must be undirected, and each edge must be represented twice (once for
+    each node). The weights should be identical, if provided.
+
+    `nodew` is a list of node weights, and must be the same size as `adjlist` if given.
+    If desired, the elements of `nodew` may be tuples of the same size (>= 1) to provided
+    multiple weights for each node. 
+
+    `nodesz` is a list of node sizes. These are relevant when doing volume-based
+    partitioning. 
+
+    Note that all weights and sizes must be non-negative integers.
+    """
+    n = len(adjlist)
+    m2 = sum(map(len, adjlist))    
+
+    xadj = (idx_t*(n+1))()
+    adjncy = (idx_t*m2)()
+    adjwgt = (idx_t*m2)()
+
+    ncon = idx_t(1)
+    if nodew:
+        if isinstance(nodew[0]):
+            vwgt = (idx_t*n)(*nodew)
+        else: # Assume a list of them
+            nw = len(nodew[0])
+            ncon = idx_t(nw)
+            vwgt = (idx_t*(nw*n))(*reduce(op.add,nodew))
+    else:
+        vwgt = None
+
+    if nodesz:
+        vsize = (idx_t*n)(*nodesz)
+    else:
+        vsize = None
+
+    xadj[0] = e = 0
+    for i, adj in enumerate(adjlist):
+        for j in adj:
+            try:
+                adjncy[e], adjwgt[e] = j
+            except TypeError:
+                adjncy[e], adjwgt[e] = j, 1
+            e += 1        
+        xadj[i+1] = e
+
+    return METIS_Graph(idx_t(n), ncon, xadj, adjncy, vwgt, vsize, adjwgt)
+
+
+
+
+### Wrapped METIS functions ###
+
+@_wrapdll(P(idx_t))
+def _METIS_SetDefaultOptions(optarray):
+    _METIS_SetDefaultOptions.call(optarray)
+
+@_wrapdll(P(idx_t), P(idx_t), P(idx_t), P(idx_t),
+    P(idx_t), P(idx_t), P(idx_t), P(idx_t), P(real_t),
+    P(real_t), P(idx_t), P(idx_t), P(idx_t))
+def _METIS_PartGraphKway(graph, nparts=2, 
+    tpwgts=None, ubvec=None, **opts):
+    """
+    Perform k-way partitioning. 
+    Returns a 2-tuple `(part, objval)`, where `part` is a list of
+    partition indices corresponding and `objval` is the value of 
+    the objective function that was minimized (either the edge cuts
+    or the total volume).
+
+    `graph` may be a NetworkX graph, an adjacency list, or a METIS_Graph 
+    named tuple. To use the named tuple approach, you'll need to
+    read the METIS manual for the meanings of the fields.
+
+    See `networkx_to_metis` for help and details on how the
+    graph is converted and how node/edge weights and sizes can
+    be specified.
+
+    See `adjlist_to_metis` for information on the use of adjacency lists.
+    The extra `nodew` and `nodesz` options of that function may be given 
+    directly to this function and will be forwarded to the converter. 
+    Alternatively, a dictionary can be provided as `graph` and its items
+    will be passed as keyword arguments.
+
+    `nparts` is the target number of partitions. You might get fewer.
+
+    You probably won't need to specify `tpwgts` and `ubvec`, but if 
+    you do, you'll read the METIS manual to see what they are. Any
+    sequence type with floats may be provided so long as the number of
+    elements is correct. If a ctypes Array is provided, it must be the
+    correct type (`metis.real_t`). 
+
+    Any additional METIS options may be specified as keyword parameters.
+    For this function, the appropriate options are:
+
+       objtype   = 'cut' or 'vol' 
+       ctype     = 'rm' or 'shem' 
+       iptype    = 'grow', 'random', 'edge', 'node'
+       rtype     = 'fm', 'greedy', 'sep2sided', 'sep1sided'
+       ncuts     = integer, number of cut attempts (default = 1)
+       niter     = integer, number of iterations (default = 10)
+       ufactor   = integer, maximum load imbalance of (1+x)/1000
+       minconn   = bool, minimize degree of subdomain graph
+       contig    = bool, force contiguous partitions
+       seed      = integer, RNG seed
+       numbering = 0 (C-style) or 1 (Fortran-style) indices
+       dbglvl    = Debug flag bitfield
+
+    See the METIS manual for specifics. 
+    """
+    options = METIS_Options(**opts)
+    if networkx and isinstance(graph, networkx.Graph):
+        graph = networkx_to_metis(graph)
+    elif isinstance(graph, (list,tuple)):
+        nodesz = opts.pop('nodesz', None)
+        nodew  = opts.pop('nodew', None)
+        graph = adjlist_to_metis(graph, nodew, nodesz)
+    elif isinstance(graph, dict):
+        # Check if this has METIS_Graph fields or an adjlist
+        if 'nvtxs' in graph:
+            graph = METIS_Graph(**graph)
+        elif 'adjlist' in graph:
+            graph = adlist_to_metis(**graph)
+
+    if tpwgts and not isinstance(tpwgts, ctypes.Array):
+        tpwgts = (real_t*len(tpwgts))(*tpwgts)
+    if ubvec and not isinstance(ubvec, ctypes.Array):
+        ubvec = (real_t*len(ubvect))(*ubvec)
+    nparts_var = idx_t(nparts)
+
+    objval = idx_t()
+    partition = (idx_t*graph.nvtxs.value)()
+
+    _METIS_PartGraphKway.call(
+        byref(graph.nvtxs), byref(graph.ncon), graph.xadj,
+        graph.adjncy, graph.vwgt, graph.vsize, graph.adjwgt,
+        byref(nparts_var), tpwgts, ubvec, options.array, 
+        byref(objval), partition)
+
+    return list(partition), objval.value
+
+@_wrapdll(P(idx_t), P(idx_t), P(idx_t), P(idx_t),
+    P(idx_t), P(idx_t), P(idx_t), P(idx_t), P(real_t),
+    P(real_t), P(idx_t), P(idx_t), P(idx_t))
+def _METIS_PartGraphRecursive(graph, nparts=2, 
+    tpwgts=None, ubvec=None, **opts):
+    """
+    Perform recursive partitioning. 
+    Returns a 2-tuple `(part, objval)`, where `part` is a list of
+    partition indices corresponding and `objval` is the value of 
+    the objective function that was minimized (either the edge cuts
+    or the total volume).
+
+    `graph` may be a NetworkX graph, an adjacency list, or a METIS_Graph 
+    named tuple. To use the named tuple approach, you'll need to
+    read the METIS manual for the meanings of the fields.
+
+    See `networkx_to_metis` for help and details on how the
+    graph is converted and how node/edge weights and sizes can
+    be specified.
+
+    See `adjlist_to_metis` for information on the use of adjacency lists.
+    The extra `nodew` and `nodesz` options of that function may be given 
+    directly to this function and will be forwarded to the converter. 
+    Alternatively, a dictionary can be provided as `graph` and its items
+    will be passed as keyword arguments.
+
+    `nparts` is the target number of partitions. You might get fewer.
+
+    You probably won't need to specify `tpwgts` and `ubvec`, but if 
+    you do, you'll read the METIS manual to see what they are. Any
+    sequence type with floats may be provided so long as the number of
+    elements is correct. If a ctypes Array is provided, it must be the
+    correct type (`metis.real_t`). 
+
+    Any additional METIS options may be specified as keyword parameters.
+    For this function, the appropriate options are:
+
+       ctype     = 'rm' or 'shem' 
+       iptype    = 'grow', 'random', 'edge', 'node'
+       rtype     = 'fm', 'greedy', 'sep2sided', 'sep1sided'
+       ncuts     = integer, number of cut attempts (default = 1)
+       niter     = integer, number of iterations (default = 10)
+       ufactor   = integer, maximum load imbalance of (1+x)/1000
+       seed      = integer, RNG seed
+       numbering = 0 (C-style) or 1 (Fortran-style) indices
+       dbglvl    = Debug flag bitfield
+
+    See the METIS manual for specifics. 
+    """
+    options = METIS_Options(**opts)
+    if networkx and isinstance(graph, networkx.Graph):
+        graph = networkx_to_metis(graph)
+    elif isinstance(graph, (list,tuple)):
+        nodesz = opts.pop('nodesz', None)
+        nodew  = opts.pop('nodew', None)
+        graph = adjlist_to_metis(graph, nodew, nodesz)
+    elif isinstance(graph, dict):
+        # Check if this has METIS_Graph fields or an adjlist
+        if 'nvtxs' in graph:
+            graph = METIS_Graph(**graph)
+        elif 'adjlist' in graph:
+            graph = adlist_to_metis(**graph)
+
+    if tpwgts and not isinstance(tpwgts, ctypes.Array):
+        tpwgts = (real_t*len(tpwgts))(*tpwgts)
+    if ubvec and not isinstance(ubvec, ctypes.Array):
+        ubvec = (real_t*len(ubvect))(*ubvec)
+    nparts_var = idx_t(nparts)
+
+    objval = idx_t()
+    partition = (idx_t*graph.nvtxs.value)()
+
+    _METIS_PartGraphRecursive.call(
+        byref(graph.nvtxs), byref(graph.ncon), graph.xadj,
+        graph.adjncy, graph.vwgt, graph.vsize, graph.adjwgt,
+        byref(nparts_var), tpwgts, ubvec, options.array, 
+        byref(objval), partition)
+
+    return list(partition), objval.value
+
+part_graph_kway = _METIS_PartGraphKway
+
+part_graph_recur = _METIS_PartGraphRecursive
+
+### End METIS wrappers. ###
+
+
+from distribute_setup import use_setuptools
+use_setuptools()
+
+from setuptools import setup
+
+from distutils.command.sdist import sdist
+from subprocess import Popen, PIPE
+
+from metis import __version__
+
+class sdist_hg(sdist):
+    user_options = sdist.user_options + [
+            ('dev', None, "Add a dev marker")
+            ]
+
+    def initialize_options(self):
+        sdist.initialize_options(self)
+        self.dev = 0
+
+    def run(self):
+        if self.dev:
+            suffix = '.dev%s' % self.get_revision()
+            self.distribution.metadata.version += suffix
+        sdist.run(self)
+
+    def get_revision(self):
+        try:
+            p = Popen(['hg', 'id', '-i'], stdout=PIPE)
+            rev = p.stdout.read().strip()
+        except:
+            print("Could not determine hg revision.")
+            rev = "deadbeef"
+        return rev
+
+setup(
+    name='metis',
+    version=__version__,
+    author="Ken Watford",
+    author_email="kwatford@gmail.com",
+    url="https://bitbucket.org/kw/metis-python",
+    download_url="https://bitbucket.org/kw/metis-python/downloads",
+    py_modules=['metis'],
+    license='MIT',
+    description="METIS wrapper using ctypes",    
+    long_description=open('README.txt').read(),
+    cmdclass={'sdist': sdist_hg},
+    classifiers = [
+        'Development Status :: 3 - Alpha',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Topic :: Software Development :: Libraries',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 3',
+        ],
+)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.