Repository: TritonDataCenter/python-manta (language: python)

Issue:
python 3 support
I have some work toward this. However, paramiko doesn't currently support Python 3 AFAICT (https://github.com/paramiko/paramiko/issues/16). That's a blocker right now.



Discussion / hints:
paramiko/paramiko#16 is closed, Py3k support is included as of 1.13.0. I'm going to see if I can get manta/mantash working.


Fix patch:
diff --git a/.gitignore b/.gitignore
index 1e12544..14d5ef0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ __pycache__
 /dist
 /MANIFEST
 /manta.egg-info
+docker-compose.yml
+_env
diff --git a/README.md b/README.md
index a63d0e0..6319eaf 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,11 @@ docs](http://apidocs.joyent.com/manta/index.html).
 
 # Current Status
 
-Tested mostly on Mac and SmartOS using Python 2.6 or 2.7. Linux should work.
-The *intention* is to support Windows as well. Python 3 is not currently
-supported (currently because the dependency paramiko does not work with
-Python 3).
+Tested mostly on Mac and SmartOS using Python 2.7. Linux should work.
+The *intention* is to support Windows as well. Python 3 support is new and we
+welcome feedback.
+
+Python 2.6 support has been dropped in python-manta 2.7.
 
 Feedback and issues here please: <https://github.com/joyent/python-manta/issues>
 
@@ -25,9 +26,12 @@ tl;dr: `pip install --upgrade manta`
 
 **SmartOS**:
 
+1. Install pip:
     pkgin install py27-pip
+    or
+    pkgin install py34-pip
 
-    # And probably libsodium and pynacl
+2. Install libsodium and pynacl
     pkgin install libsodium
     SODIUM_INSTALL=system pip install pynacl
 
@@ -39,6 +43,8 @@ tl;dr: `pip install --upgrade manta`
 
 **Ubuntu**:
 
+    sudo apt-get install python3-pip 
+    or
     sudo apt-get install python-pip
 
 Others? Please [let me know](https://github.com/joyent/python-manta/issues/new)
@@ -46,6 +52,7 @@ if there are better instructions that I can provide for your system, so I can
 add them here.
 
 
+
 ## 1. install python-manta
 
 The preferred way is:
diff --git a/bin/mantash b/bin/mantash
index 4f63d82..d133f3f 100755
--- a/bin/mantash
+++ b/bin/mantash
@@ -1,7 +1,9 @@
 #!/usr/bin/env python
-
 """Manta shell."""
 
+from __future__ import absolute_import
+from __future__ import print_function
+
 import sys
 import os
 from posixpath import dirname as udirname, join as ujoin, \
@@ -10,6 +12,7 @@ from posixpath import dirname as udirname, join as ujoin, \
 import datetime
 import logging
 import json
+import base64
 from pprint import pprint, pformat
 import re
 import time
@@ -34,16 +37,12 @@ if _dev_package_dir in sys.path:
     sys.path.remove(_dev_package_dir)
 del _dev_package_dir
 
-
-
 #---- Python version compat
 
 try:
-    from urllib.parse import urlparse # python3
+    from urllib.parse import urlparse  # python3
 except ImportError:
-    from urlparse import urlparse # python2
-
-
+    from urlparse import urlparse  # python2
 
 #---- globals
 
@@ -55,24 +54,22 @@ log = logging.getLogger("mantash")
 CACHE_DIR = appdirs.user_cache_dir("mantash-%s" % os.geteuid(), "Joyent")
 HTTP_CACHE_DIR = os.path.join(CACHE_DIR, "http")
 
-USER_AGENT = "mantash/%s (%s) Python/%s" % (
-    manta.__version__, sys.platform, sys.version.split(None, 1)[0])
+USER_AGENT = "mantash/%s (%s) Python/%s" % (manta.__version__, sys.platform,
+                                            sys.version.split(None, 1)[0])
 KNOWN_USERS_PATH = os.path.join(CACHE_DIR, 'known-users.json')
 
 DEFAULT_PS1 = r'[\m\w]$ '
 
-
-
-
 #---- errors
 
+
 class MantashError(Exception):
     pass
 
+
 class MantaObjectDoesNotExist(MantashError):
     pass
 
-
 #---- the shell
 
 
@@ -80,14 +77,17 @@ def optparser_setattr(**kwargs):
     """Set attrs on the function's `optparser` attribute.
 
     This is modelled off `cmdln.option()`."""
+
     def decorate(f):
         if not hasattr(f, "optparser"):
             f.optparser = SubCmdOptionParser()
-        for k,v in kwargs.items():
+        for k, v in kwargs.items():
             setattr(f.optparser, k, v)
         return f
+
     return decorate
 
+
 class Mantash(cmdln.Cmdln):
     r"""${name} -- a Manta shell
 
@@ -117,41 +117,64 @@ class Mantash(cmdln.Cmdln):
     name = "mantash"
     version = manta.__version__
     manta_url = None
-    cwd = None          # initialized in `postoptparse`
-    last_cwd = None     # ditto
+    cwd = None  # initialized in `postoptparse`
+    last_cwd = None  # ditto
 
     def get_optparser(self):
         parser = cmdln.Cmdln.get_optparser(self)
         #parser.add_option("--version", action="store_true",
         #    help="print mantash version and exit")
-        parser.add_option("-v", "--verbose", dest="verbose",
-            action="store_true", help="Verbose/debug logging.")
-        parser.add_option("-u", "--url", dest="manta_url",
-            help="Manta URL. Environment: MANTA_URL=URL",
-            default=os.environ.get("MANTA_URL"))
-        parser.add_option("-a", "--account", dest="account",
+        parser.add_option("-v",
+                          "--verbose",
+                          dest="verbose",
+                          action="store_true",
+                          help="Verbose/debug logging.")
+        parser.add_option("-u",
+                          "--url",
+                          dest="manta_url",
+                          help="Manta URL. Environment: MANTA_URL=URL",
+                          default=os.environ.get("MANTA_URL"))
+        parser.add_option(
+            "-a",
+            "--account",
+            dest="account",
             help="Manta account (login name). Environment: MANTA_USER=ACCOUNT",
             default=os.environ.get("MANTA_USER"))
-        parser.add_option("--user", "--subuser", dest="subuser",
+        parser.add_option(
+            "--user",
+            "--subuser",
+            dest="subuser",
             help="Manta user (login name). Environment: MANTA_SUBUSER=USER",
             default=os.environ.get("MANTA_SUBUSER", None))
-        parser.add_option("--role", dest="role",
-                          help=("Assume a role. Use multiple times or once with "
-                                "a list. Environment: MANTA_ROLE=ROLE,ROLE..."),
-            default=os.environ.get("MANTA_ROLE", None))
-        parser.add_option("-k", "--keyId", dest="key_id",
+        parser.add_option("--role",
+                          dest="role",
+                          help=(
+                              "Assume a role. Use multiple times or once with "
+                              "a list. Environment: MANTA_ROLE=ROLE,ROLE..."),
+                          default=os.environ.get("MANTA_ROLE", None))
+        parser.add_option(
+            "-k",
+            "--keyId",
+            dest="key_id",
             help="SSH key fingerprint (or path to private key file). See "
-                "note below. Environment: MANTA_KEY_ID=FINGERPRINT",
+            "note below. Environment: MANTA_KEY_ID=FINGERPRINT",
             default=os.environ.get("MANTA_KEY_ID"))
-        parser.add_option("-i", "--insecure", action="store_true",
+        parser.add_option(
+            "-i",
+            "--insecure",
+            action="store_true",
             dest="insecure",
             help="Do not validate SSL/TLS certificate. Not recommended but "
-                "useful for dev. Environment: MANTA_TLS_INSECURE=1",
+            "useful for dev. Environment: MANTA_TLS_INSECURE=1",
             default=(os.environ.get("MANTA_TLS_INSECURE") == "1"))
-        parser.add_option("-C", dest="cd", metavar="DIRECTORY",
-            help="first change to the given directory")
-        parser.add_option("--drop-cache", dest="drop_cache", action="store_true",
-            help="drop the current HTTP cache before starting")
+        parser.add_option("-C",
+                          dest="cd",
+                          metavar="DIRECTORY",
+                          help="first change to the given directory")
+        parser.add_option("--drop-cache",
+                          dest="drop_cache",
+                          action="store_true",
+                          help="drop the current HTTP cache before starting")
         return parser
 
     def postoptparse(self):
@@ -175,17 +198,20 @@ class Mantash(cmdln.Cmdln):
 
         no_auth = (os.environ.get("MANTA_NO_AUTH", "false") == "true")
         if not self.options.manta_url:
-            raise MantashError("no Manta URL: use '--url' or 'MANTA_URL' envvar")
+            raise MantashError(
+                "no Manta URL: use '--url' or 'MANTA_URL' envvar")
         if not self.options.account:
-            raise MantashError("no Manta account: use '--account' or 'MANTA_USER' envvar")
+            raise MantashError(
+                "no Manta account: use '--account' or 'MANTA_USER' envvar")
         if not no_auth and not self.options.key_id:
-            raise MantashError("no Manta key ID: use '-i' or 'MANTA_KEY_ID' envvar")
+            raise MantashError(
+                "no Manta key ID: use '-i' or 'MANTA_KEY_ID' envvar")
         self.manta_url = self.options.manta_url
 
         self.ps1 = os.environ.get("MANTASH_PS1", DEFAULT_PS1)
 
         if (not self.manta_url.startswith('http://') and
-            not self.manta_url.startswith('https://')):
+                not self.manta_url.startswith('https://')):
             self.manta_url = 'https://' + self.manta_url
         if self.manta_url.endswith('/'):
             self.manta_url = self.manta_url[:-1]
@@ -201,12 +227,15 @@ class Mantash(cmdln.Cmdln):
             signer = None
         else:
             signer = manta.CLISigner(self.options.key_id)
-        self.client = manta.MantaClient(self.manta_url, self.account,
+        self.client = manta.MantaClient(
+            self.manta_url,
+            self.account,
             subuser=self.subuser,
             role=self.role,
             signer=signer,
             disable_ssl_certificate_validation=self.options.insecure,
-            user_agent=USER_AGENT, verbose=self.options.verbose)
+            user_agent=USER_AGENT,
+            verbose=self.options.verbose)
 
         self.do_help.aliases.append("man")
 
@@ -215,7 +244,7 @@ class Mantash(cmdln.Cmdln):
         try:
             f = codecs.open(KNOWN_USERS_PATH, 'r', 'utf8')
             self._known_users = json.load(f, encoding='utf8')
-        except Exception, ex:
+        except Exception as ex:
             pass
         finally:
             if f:
@@ -234,6 +263,7 @@ class Mantash(cmdln.Cmdln):
             print(doc)
         else:
             cmdln.Cmdln.do_help(self, argv)
+
     do_help.aliases = cmdln.Cmdln.do_help.aliases
 
     def _update_prompt(self):
@@ -268,12 +298,17 @@ class Mantash(cmdln.Cmdln):
             'e': '\033',
             '\\': '\\',
         }
+
         def repl(match):
             return codes[match.group(1)]
+
         allkeys = ''.join(codes.keys()).replace('\\', '\\\\')
         uprompt = re.sub(r'\\([%s])' % allkeys, repl, self.ps1)
-        self._prompt_str = uprompt.encode(
-            sys.stdout.encoding or 'UTF-8', 'replace')
+        self._prompt_str = uprompt
+        '''
+        self._prompt_str = uprompt.encode(sys.stdout.encoding or 'UTF-8',
+                                          'replace')
+        '''
 
     def do_lpwd(self, subcmd, opts):
         """${cmd_name}: print the local cwd"""
@@ -324,6 +359,7 @@ class Mantash(cmdln.Cmdln):
                 raise
 
     _is_glob_re = re.compile(r'(?<!\\)[*?\[]')
+
     def _ls_path(self, path, listDirs):
         """Get the set of directory entries (dirents) matching the
         given path, which may be a glob pattern.
@@ -340,8 +376,10 @@ class Mantash(cmdln.Cmdln):
               {'bar.py': {'mtime': 1341422663102L, 'type': 'object'},
                'baz': {'mtime': 1341422663102L, 'type': 'directory'}})]
         """
-        upath = uexpanduser(path, self.home)    # user-expanded
-        npath = unormpath(upath)                # normalized
+        if isinstance(path, bytes):
+            path = path.decode("utf-8")
+        upath = uexpanduser(path, self.home)  # user-expanded
+        npath = unormpath(upath)  # normalized
         abspath = unormpath(ujoin(self.cwd, npath))
         absparts = abspath.split('/')
         parts = upath.split('/')
@@ -349,7 +387,7 @@ class Mantash(cmdln.Cmdln):
         head = '/'.join(absparts[:3])
         if self._is_glob_re.search(head):
             raise MantashError("cannot use glob chars in first two parts "
-                "of a Manta path: %r" % head)
+                               "of a Manta path: %r" % head)
 
         DEBUG = False
         if DEBUG:
@@ -371,26 +409,29 @@ class Mantash(cmdln.Cmdln):
                 # TODO: Just the user for now. Add cached visited public
                 # users eventually.
                 dirents = {
-                    self.account: {"name": self.account, "type": "directory"}
+                    self.account: {"name": self.account,
+                                   "type": "directory"}
                 }
             hits = [(is_dir, upath, dirents)]
 
-        elif len(absparts) == 2:   # '/USER'
+        elif len(absparts) == 2:  # '/USER'
             # '/USER'. Just presume that user exists for now.
             is_dir = True
             if listDirs:
                 dirents = {abspath: {"name": absparts[1], "type": "directory"}}
             elif absparts[1] == self.account:
                 dirents = {
-                    "stor": {"name": "stor", "type": "directory"},
-                    "public": {"name": "public", "type": "directory"},
-                    "jobs": {"name": "jobs", "type": "directory"},
-                    "reports": {"name": "reports", "type": "directory"},
+                    "stor": {"name": "stor",
+                             "type": "directory"},
+                    "public": {"name": "public",
+                               "type": "directory"},
+                    "jobs": {"name": "jobs",
+                             "type": "directory"},
+                    "reports": {"name": "reports",
+                                "type": "directory"},
                 }
             else:
-                dirents = {
-                    "public": {"name": "public", "type": "directory"}
-                }
+                dirents = {"public": {"name": "public", "type": "directory"}}
             hits = [(is_dir, upath, dirents)]
 
         elif not is_glob:
@@ -399,7 +440,7 @@ class Mantash(cmdln.Cmdln):
                 # '/USER/{stor,public,jobs,reports}'
                 if absparts[2] not in ("stor", "public", "jobs", "reports"):
                     raise MantashError("%s: invalid manta path" %
-                        (path or '/'))
+                                       (path or '/'))
                 dirent = {"name": absparts[2], "type": "directory"}
             else:
                 # Ensure it exists.
@@ -412,29 +453,24 @@ class Mantash(cmdln.Cmdln):
                     dirent = pdirents.get(name)
             if not dirent:
                 raise MantashError("%s: no such object or directory" %
-                    (path or '/'))
+                                   (path or '/'))
             if DEBUG:
                 print("  dirent: %r" % dirent)
-
             if dirent.get("type") == "directory":
                 is_dir = True
                 if listDirs:
-                    dirents = {
-                        upath: dirent
-                    }
+                    dirents = {upath: dirent}
                 else:
                     dirents = self.client.ls(abspath)
             else:
                 is_dir = False
-                dirents = {
-                    upath: dirent
-                }
+                dirents = {upath: dirent}
             hits = [(is_dir, upath, dirents)]
 
         else:
             # Work up to first dir with a glob char.
             lead = absparts[:3]  # no glob in first two segments allowed
-            for i in range(3, len(absparts)-1):
+            for i in range(3, len(absparts) - 1):
                 if self._is_glob_re.search(absparts[i]):
                     break
                 lead.append(absparts[i])
@@ -457,28 +493,33 @@ class Mantash(cmdln.Cmdln):
             matches = {'/'.join(lead): {"type": "directory"}}
             for i in range(len(lead), len(absparts)):
                 p = absparts[i]
-                patterns = [unormpath(ujoin(m, p)) for m,dirent in matches.items()
-                    if dirent["type"] == "directory"]
+                patterns = [unormpath(ujoin(m, p))
+                            for m, dirent in matches.items()
+                            if dirent["type"] == "directory"]
                 if DEBUG:
-                    print("  glob part %r: patterns %s from matches %r" % (p, patterns, matches))
+                    print("  glob part %r: patterns %s from matches %r" %
+                          (p, patterns, matches))
                 matches = {}
                 for pattern in patterns:
                     parent = udirname(pattern)
                     basepat = ubasename(pattern)
                     dirents = self._get_dir(ujoin(self.cwd, parent))
                     if DEBUG:
-                        print("    pattern %r:\n\tparent %r\n\tbasepat %r\n\tdirents %r" % (pattern, parent, basepat, dirents))
+                        print(
+                            "    pattern %r:\n\tparent %r\n\tbasepat %r\n\tdirents %r"
+                            % (pattern, parent, basepat, dirents))
                     if dirents:
                         for name, dirent in dirents.items():
                             if fnmatch.fnmatchcase(name, basepat):
                                 matches[ujoin(parent, name)] = dirent
                             elif DEBUG:
-                                print("    discard %r (does not match %r)" % (name, basepat))
+                                print("    discard %r (does not match %r)" %
+                                      (name, basepat))
                 if DEBUG:
                     print("    new matches: %r" % matches)
                 if not matches:
                     raise MantashError("%s: no such object or directory" %
-                        (path or '/'))
+                                       (path or '/'))
 
             # Any leaf directories are recursed into... unless `ls -d`.
             #
@@ -486,7 +527,7 @@ class Mantash(cmdln.Cmdln):
             # again (if the given start path was relative).
             hits = []
             if not listDirs:
-                for name,dirent in matches.items():
+                for name, dirent in dict(matches).items():
                     if dirent["type"] == "directory":
                         if DEBUG:
                             print("    descend into dir %r" % name)
@@ -501,18 +542,21 @@ class Mantash(cmdln.Cmdln):
                         relkey = key.replace(absprefix, prefix, 1)
                         relmatches[relkey] = matches[key]
                 hits.append((False, upath, relmatches))
-
         if hits and hits[0][-1]:
             self._visited_known_mpath(abspath)
         if DEBUG:
             print("  hits:\n%s" % _indent(pformat(hits)))
         return hits
 
-    @cmdln.option("-h", action="store_true", dest="human",
-        help="display human-readable sizes")
+    @cmdln.option("-h",
+                  action="store_true",
+                  dest="human",
+                  help="display human-readable sizes")
     @optparser_setattr(conflict_handler="resolve")
-    @cmdln.option("-s", action="store_true", dest="summary",
-        help="display an entry for each specified file")
+    @cmdln.option("-s",
+                  action="store_true",
+                  dest="summary",
+                  help="display an entry for each specified file")
     def do_du(self, subcmd, opts, *paths):
         """disk usage
 
@@ -535,9 +579,9 @@ class Mantash(cmdln.Cmdln):
         for path in paths:
             npath = unormpath(ujoin(self.cwd, uexpanduser(path, self.home)))
             d, b = usplit(npath)
-            if '*' in b or '?' in b or '[' in b: # we have a glob
+            if '*' in b or '?' in b or '[' in b:  # we have a glob
                 candidates = self.client.ls(d)
-                for match in fnmatch.filter(candidates.keys(), b):
+                for match in fnmatch.filter(list(candidates), b):
                     e = candidates[match]
                     e["path"] = ujoin(d, match)
                     dirents.append(e)
@@ -570,19 +614,28 @@ class Mantash(cmdln.Cmdln):
             else:
                 du(dirent)
 
-
-    @cmdln.option("-h", action="store_true", dest="human",
-        help="display human-readable sizes")
+    @cmdln.option("-h",
+                  action="store_true",
+                  dest="human",
+                  help="display human-readable sizes")
     @cmdln.option("-a", action="store_true", help=optparse.SUPPRESS_HELP)
-    @cmdln.option("-l", action="store_true", dest="long",
-        help="list in long format")
-    @cmdln.option("-d", action="store_true", dest="dir",
-        help="list directory names, do not search recursively")
-    @cmdln.option("-F", action="store_true", dest="format",
-        help="display a '/' after a directory name")
+    @cmdln.option("-l",
+                  action="store_true",
+                  dest="long",
+                  help="list in long format")
+    @cmdln.option("-d",
+                  action="store_true",
+                  dest="dir",
+                  help="list directory names, do not search recursively")
+    @cmdln.option("-F",
+                  action="store_true",
+                  dest="format",
+                  help="display a '/' after a directory name")
     @optparser_setattr(conflict_handler="resolve")
-    @cmdln.option("-j", "--json", action="store_true",
-        help="display the listing in JSON")
+    @cmdln.option("-j",
+                  "--json",
+                  action="store_true",
+                  help="display the listing in JSON")
     def do_ls(self, subcmd, opts, *paths):
         """list objects, directories and links
 
@@ -594,13 +647,16 @@ class Mantash(cmdln.Cmdln):
         retval = None  # set to 1 for any errors
 
         # Gather list of things to print.
-        paths = paths or ['']
+        paths = paths or [b'']
         all_to_print = []  # list of (<is-dir>, <path>, <dirents>)
         for path in paths:
-            upath = path.decode(sys.stdin.encoding)
+            if isinstance(path, bytes):
+                upath = path.decode(sys.stdin.encoding)
+            else:
+                upath = path
             try:
-                to_print = self._ls_path(path, opts.dir)
-            except (MantaError, MantashError):
+                to_print = self._ls_path(upath, opts.dir)
+            except (MantaError, MantashError, Exception):
                 _, ex, _ = sys.exc_info()
                 log.error(ex)
                 retval = 1
@@ -610,8 +666,9 @@ class Mantash(cmdln.Cmdln):
         # Do non-dirs first, as does `ls dirA fileB dirC fileD`.
         if opts.json:
             listing = {}
-        for i, (is_dir, path, dirents) in enumerate(
-                sorted(all_to_print, key=itemgetter(0))):
+        for i, (
+                is_dir, path, dirents
+        ) in enumerate(sorted(all_to_print, key=itemgetter(0))):
             if opts.json:
                 listing[path] = dirents
                 continue
@@ -619,20 +676,25 @@ class Mantash(cmdln.Cmdln):
                 if i > 0:
                     print("")
                 print("%s:" % path)
-            for name, dirent in sorted(dirents.iteritems()):
-                self._ls_print_dirent(name, dirent, long=opts.long,
-                    format=opts.format, human=opts.human)
+            for name, dirent in sorted(dirents.items()):
+                self._ls_print_dirent(name,
+                                      dirent,
+                                      long=opts.long,
+                                      format=opts.format,
+                                      human=opts.human)
         if opts.json:
             print(json.dumps(listing, indent=2))
         return retval
 
-    def _ls_print_dirent(self, name, dirent, long=False, format=False,
+    def _ls_print_dirent(self,
+                         name,
+                         dirent,
+                         long=False,
+                         format=False,
                          human=False):
+
         if format:
-            name += {
-                "directory": "/",
-                "link": "@"
-            }.get(dirent["type"], "")
+            name += {"directory": "/", "link": "@"}.get(dirent["type"], "")
         if long:
             if human:
                 dirent_fmt = "%9s  %s  %04s  %24s  %s"
@@ -652,8 +714,8 @@ class Mantash(cmdln.Cmdln):
             #    # AFAIK Python's strptime doesn't support milliseconds.
             #    mtime = time.asctime(
             #        time.strptime(dirent["mtime"], "%Y-%m-%dT%H:%M:%S.721Z"))
-            print(dirent_fmt % (
-                dirent.get("type", "unknown"), self.account, sz, mtime, name))
+            print(dirent_fmt % (dirent.get("type", "unknown"), self.account,
+                                sz, mtime, name))
         else:
             print(name)
 
@@ -688,8 +750,8 @@ class Mantash(cmdln.Cmdln):
         area = parts[2]
         if area != 'public':
             area = 'private'
-        if (area == 'public' and user not in self._known_users
-            or self._known_users.get('user') != 'private'):
+        if (area == 'public' and user not in self._known_users or
+                self._known_users.get('user') != 'private'):
             self._known_users[user] = area
             f = None
             try:
@@ -707,7 +769,10 @@ class Mantash(cmdln.Cmdln):
         Usage:
             ${cmd_name} [DIRECTORY]
         """
-        udirectory = directory.decode(sys.stdin.encoding)
+        if isinstance(directory, bytes):
+            udirectory = directory.decode(sys.stdin.encoding)
+        else:
+            udirectory = directory
         ndir = unormpath(ujoin(self.cwd, uexpanduser(udirectory, self.home)))
         nparts = ndir.split('/')
         last_cwd = self.cwd
@@ -728,7 +793,7 @@ class Mantash(cmdln.Cmdln):
             name = ubasename(ndir)
             try:
                 dirents = self.client.ls(parent)
-            except manta.MantaAPIError, ex:
+            except manta.MantaAPIError as ex:
                 if ex.code == 'ResourceNotFound':
                     log.error("%s: no such directory", directory)
                     return 1
@@ -750,8 +815,8 @@ class Mantash(cmdln.Cmdln):
         try:
             DEBUG = False
             if DEBUG:
-                print("\n-- completedefault:",)
-                print("  %r" % ((text, line, begidx, endidx),))
+                print("\n-- completedefault:", )
+                print("  %r" % ((text, line, begidx, endidx), ))
             subcmd = line and line.split(None, 1)[0]
             start = line[begidx:endidx]
             if DEBUG:
@@ -797,19 +862,19 @@ class Mantash(cmdln.Cmdln):
                 parent = unormpath(ujoin(self.cwd, udirname(start)))
                 if parent == '/':
                     matches = ['/' + u + '/' for u in self._known_users.keys()
-                        if u.startswith(startbase)]
+                               if u.startswith(startbase)]
                 elif parent.count('/') == 1:
                     # Infer if have private access from previous successful
                     # operations on files in private dirs.
                     account = self.cwd.split('/')[1]
-                    priv_access = (account == self.account
-                        or self._known_users.get(account) == 'private')
+                    priv_access = (account == self.account or
+                                   self._known_users.get(account) == 'private')
                     if priv_access:
                         dirs = ['jobs', 'public', 'reports', 'stor']
                     else:
                         dirs = ['public']
                     matches = [ujoin(startdir, n) + '/' for n in dirs
-                        if n.startswith(startbase)]
+                               if n.startswith(startbase)]
                 else:
                     dirents = self._get_dir(parent)
                     if DEBUG:
@@ -818,29 +883,34 @@ class Mantash(cmdln.Cmdln):
                         matches = []
                     elif mode == "manta-dir":
                         matches = [
-                            ujoin(startdir, n) + '/' for n,d in dirents.items()
-                            if n.startswith(startbase)
-                            and n.startswith('.') == startbase.startswith('.')
-                            and d["type"] == "directory"]
+                            ujoin(startdir, n) + '/'
+                            for n, d in dirents.items()
+                            if n.startswith(startbase) and n.startswith('.') ==
+                            startbase.startswith('.') and d["type"] ==
+                            "directory"
+                        ]
                     else:
                         matches = [
-                            ujoin(startdir, n) + (d["type"] == "directory" and "/" or "")
-                            for n,d in dirents.items()
-                            if n.startswith(startbase)
-                            and n.startswith('.') == startbase.startswith('.')]
+                            ujoin(startdir, n) +
+                            (d["type"] == "directory" and "/" or "")
+                            for n, d in dirents.items()
+                            if n.startswith(startbase) and n.startswith('.') ==
+                            startbase.startswith('.')
+                        ]
             elif mode in ("local-path", "local-dir"):
                 start = os.path.expanduser(start)
                 startdir = os.path.dirname(start)
                 startbase = os.path.basename(start)
                 parent = os.path.join(os.getcwd(), startdir)
-                matches = [os.path.join(startdir, n) for n in os.listdir(parent)
-                           if n.startswith(startbase)
-                           and not n.startswith('.')]
+                matches = [os.path.join(startdir, n)
+                           for n in os.listdir(parent)
+                           if n.startswith(startbase) and not n.startswith('.')
+                           ]
                 if mode == "local-dir":
                     matches = [m + '/' for m in matches if os.path.isdir(m)]
                 else:
                     matches = [m + (os.path.isdir(m) and '/' or '')
-                        for m in matches]
+                               for m in matches]
             if startbase == '.':
                 matches.append('.')
                 matches.append('..')
@@ -912,7 +982,7 @@ class Mantash(cmdln.Cmdln):
         """
         retval = None
         head = None
-        n = 10   # TODO: option for '-n'. optional '-10' support?
+        n = 10  # TODO: option for '-n'. optional '-10' support?
         for i, path in enumerate(paths):
             #TODO: check its dir to make sure it is an object
             npath = unormpath(ujoin(self.cwd, uexpanduser(path, self.home)))
@@ -930,8 +1000,8 @@ class Mantash(cmdln.Cmdln):
             content = self.client.get(npath)
             if len(paths) > 1:
                 if i != 0:
-                    print
-                print '==> %s <==' % npath
+                    print('')
+                print('==> %s <==' % npath)
             head = ''.join(content.splitlines(True)[:n])
             sys.stdout.write(head)
             if head and not head.endswith('\n'):
@@ -948,7 +1018,7 @@ class Mantash(cmdln.Cmdln):
         """
         retval = None
         tail = None
-        n = 10   # TODO: option for '-n'. optional '-10' support?
+        n = 10  # TODO: option for '-n'. optional '-10' support?
         for i, path in enumerate(paths):
             #TODO: check its dir to make sure it is an object
             npath = unormpath(ujoin(self.cwd, uexpanduser(path, self.home)))
@@ -967,14 +1037,13 @@ class Mantash(cmdln.Cmdln):
             if len(paths) > 1:
                 if i != 0:
                     print
-                print '==> %s <==' % npath
+                print('==> %s <==' % npath)
             tail = ''.join(content.splitlines(True)[-n:])
             sys.stdout.write(tail)
             if tail and not tail.endswith('\n'):
                 sys.stdout.write('\n')
         return retval
 
-
     def do_json(self, subcmd, opts, path):
         """Download a Manta objects and JSON pretty-print it.
         Note: This is a precursor to nicely passing this to a full `json`.
@@ -1014,7 +1083,13 @@ class Mantash(cmdln.Cmdln):
 
         ${cmd_option_list}
         """
-        from cStringIO import StringIO
+        try:
+            # Python 2
+            from StringIO import StringIO as create_stream
+        except ImportError:
+            # Python 3
+            from io import BytesIO as create_stream
+
         import gzip
 
         retval = None
@@ -1034,11 +1109,11 @@ class Mantash(cmdln.Cmdln):
                 retval = 1
                 continue
             content = self.client.get(npath)
-            f = StringIO(content)
+            f = create_stream(content)
             unzipper = gzip.GzipFile(npath, 'rb', fileobj=f)
-            sys.stdout.write(unzipper.read())
+            sys.stdout.write(unzipper.read().decode('utf-8'))
             f.close()
-        if content and not content.endswith('\n'):
+        if content and not content.endswith(b'\n'):
             sys.stdout.write('\n')
         return retval
 
@@ -1050,18 +1125,23 @@ class Mantash(cmdln.Cmdln):
 
         ${cmd_option_list}
         """
-        upath = path.decode(sys.stdin.encoding)
-        npath = unormpath(ujoin(self.cwd, uexpanduser(upath, self.home)))
+        if isinstance(path, bytes):
+            upath = path.decode('utf-8')
+        else:
+            upath = path
+        npath = unormpath(ujoin(self.cwd, uexpanduser(path, self.home)))
         url = self.manta_url + npath
-        webbrowser.open(url.encode('utf-8'))
+        webbrowser.open(url)
 
     def do_exit(self, subcmd, opts):
         """${cmd_name}: exit the shell"""
         print("exit")
         self.stop = True
 
-    @cmdln.option("-p", dest="parents", action="store_true",
-        help="create intermediate directories as require")
+    @cmdln.option("-p",
+                  dest="parents",
+                  action="store_true",
+                  help="create intermediate directories as require")
     def do_mkdir(self, subcmd, opts, *paths):
         """create a directory
 
@@ -1084,19 +1164,34 @@ class Mantash(cmdln.Cmdln):
                 retval = 1
         return retval
 
-    @cmdln.option("-v", "--verbose", action="store_true",
+    @cmdln.option(
+        "-v",
+        "--verbose",
+        action="store_true",
         help="show files (and content-type) as they are being copied")
-    @cmdln.option("-d", "--durability-level",
+    @cmdln.option(
+        "-d",
+        "--durability-level",
         help="Tell Manta the number of copies to keep. Default is 2.")
-    @cmdln.option("-t", "--content-type",
+    @cmdln.option(
+        "-t",
+        "--content-type",
         help="specify content-type, else this will attempt to guess based "
-            "on the filename. This 'wins' over '-b'.")
-    @cmdln.option("-b", "--binary", action="store_true",
-        help="disable content-type guessing and use 'application/octet-stream'")
-    @cmdln.option("-R", "-r", dest="recursive", action="store_true",
-        help="recursively copy a source directory")
-    @cmdln.option("--dry-run", action="store_true",
-        help="do a dry-run, implies '--verbose'")
+        "on the filename. This 'wins' over '-b'.")
+    @cmdln.option(
+        "-b",
+        "--binary",
+        action="store_true",
+        help="disable content-type guessing and use 'application/octet-stream'"
+    )
+    @cmdln.option("-R",
+                  "-r",
+                  dest="recursive",
+                  action="store_true",
+                  help="recursively copy a source directory")
+    @cmdln.option("--dry-run",
+                  action="store_true",
+                  help="do a dry-run, implies '--verbose'")
     def do_put(self, subcmd, opts, *paths):
         """put a local file to manta
 
@@ -1135,8 +1230,8 @@ class Mantash(cmdln.Cmdln):
 
         # Get info on dest path
         # TODO: could switch to self._manta_isdir()
-        dst_realpath = unormpath(ujoin(self.cwd,
-            uexpanduser(dst_path, self.home)))
+        dst_realpath = unormpath(ujoin(self.cwd, uexpanduser(dst_path,
+                                                             self.home)))
         dst_parts = dst_realpath.split('/')
         if len(dst_parts) <= 3:
             dst_is_existing_dir = True
@@ -1147,8 +1242,8 @@ class Mantash(cmdln.Cmdln):
             if dirents is None:
                 log.error("%s: no such remote directory", dst_parent)
                 return 1
-            dst_is_existing_dir = (dst_base in dirents
-                and dirents[dst_base]["type"] == "directory")
+            dst_is_existing_dir = (dst_base in dirents and
+                                   dirents[dst_base]["type"] == "directory")
 
         # Sanity checks on args.
         if len(src_paths) > 1 or dst_path.endswith('/'):
@@ -1163,15 +1258,16 @@ class Mantash(cmdln.Cmdln):
             elif opts.binary:
                 content_type = "application/octet-stream"
             else:
-                content_type = (mimetypes.guess_type(src_file)[0]
-                    or "application/octet-stream")
+                content_type = (mimetypes.guess_type(src_file)[0] or
+                                "application/octet-stream")
 
             if opts.verbose:
                 log.info("put %s %s  # %s", src_file, dst_file, content_type)
             if not opts.dry_run:
-                self.client.put(dst_file, path=src_file,
-                    content_type=content_type,
-                    durability_level=opts.durability_level)
+                self.client.put(dst_file,
+                                path=src_file,
+                                content_type=content_type,
+                                durability_level=opts.durability_level)
 
         # Copy the files.
         retval = None
@@ -1179,7 +1275,8 @@ class Mantash(cmdln.Cmdln):
             if not opts.recursive:
                 # Must be regular file.
                 if not os.path.isfile(src_path):
-                    log.error("%s is not a regular file (not copied)", src_path)
+                    log.error("%s is not a regular file (not copied)",
+                              src_path)
                     retval = 1
                     continue
                 if dst_is_existing_dir:
@@ -1199,35 +1296,44 @@ class Mantash(cmdln.Cmdln):
                     if not opts.dry_run:
                         self.client.mkdir(dst_realpath)
 
-                rel_prefix = ((dst_is_existing_dir and not src_path.endswith('/'))
-                    and os.path.basename(src_path) + '/' or '')
+                rel_prefix = (
+                    (dst_is_existing_dir and not src_path.endswith('/')) and
+                    os.path.basename(src_path) + '/' or '')
                 if rel_prefix:
                     if not opts.dry_run:
                         self.client.mkdir(ujoin(dst_realpath, rel_prefix))
                 norm_src_path = src_path.rstrip('/')
                 for dirpath, dirnames, filenames in os.walk(norm_src_path):
-                    reldirpath = unormpath(
-                        rel_prefix + dirpath[len(norm_src_path)+1:])
+                    reldirpath = unormpath(rel_prefix + dirpath[len(
+                        norm_src_path) + 1:])
                     for filename in filenames:
-                        put_file(os.path.join(dirpath, filename), unormpath(
-                            ujoin(dst_realpath, reldirpath, filename)))
+                        put_file(
+                            os.path.join(dirpath, filename), unormpath(ujoin(
+                                dst_realpath, reldirpath, filename)))
                     if not opts.dry_run:
                         for dirname in dirnames:
-                            self.client.mkdir(unormpath(
-                                ujoin(dst_realpath, reldirpath, dirname)))
+                            self.client.mkdir(unormpath(ujoin(
+                                dst_realpath, reldirpath, dirname)))
             else:
                 log.error("%s is not a regular file or directory (not copied)",
-                    src_path)
+                          src_path)
                 retval = 1
                 continue
         return retval
 
-    @cmdln.option("-v", "--verbose", action="store_true",
+    @cmdln.option(
+        "-v",
+        "--verbose",
+        action="store_true",
         help="show files (and content-type) as they are being copied")
-    @cmdln.option("-R", "-r", dest="recursive", action="store_true",
-        help="recursively copy a source directory")
-    @cmdln.option("--dry-run", action="store_true",
-        help="do a dry-run, implies '--verbose'")
+    @cmdln.option("-R",
+                  "-r",
+                  dest="recursive",
+                  action="store_true",
+                  help="recursively copy a source directory")
+    @cmdln.option("--dry-run",
+                  action="store_true",
+                  help="do a dry-run, implies '--verbose'")
     def do_get(self, subcmd, opts, *paths):
         """get a file from manta
 
@@ -1269,8 +1375,8 @@ class Mantash(cmdln.Cmdln):
         #   put -r file1 dir1 dir2   assert dir2 is a dir
 
         # Get info on dest path
-        dst_normpath = os.path.normpath(os.path.join(os.getcwd(),
-            os.path.expanduser(dst_path)))
+        dst_normpath = os.path.normpath(os.path.join(os.getcwd(
+        ), os.path.expanduser(dst_path)))
         dst_is_existing_dir = os.path.isdir(dst_normpath)
 
         # Sanity checks on args.
@@ -1289,13 +1395,14 @@ class Mantash(cmdln.Cmdln):
         # Copy the files.
         retval = None
         for src_path in src_paths:
-            src_npath = unormpath(ujoin(self.cwd,
-                uexpanduser(src_path, self.home)))
+            src_npath = unormpath(ujoin(self.cwd, uexpanduser(src_path,
+                                                              self.home)))
             src_type = self.client.type(src_npath)
             if not opts.recursive:
                 # Must be manta object (as opposed to a dir).
                 if src_type != "object":
-                    log.error("%s is not a Manta object (not copied)", src_path)
+                    log.error("%s is not a Manta object (not copied)",
+                              src_path)
                     retval = 1
                     continue
                 if dst_is_existing_dir:
@@ -1316,21 +1423,22 @@ class Mantash(cmdln.Cmdln):
                         log.info("mkdir %s", dst_path)
                         os.mkdir(dst_path)
 
-                rel_prefix = ((dst_is_existing_dir and not src_path.endswith('/'))
-                    and ubasename(src_npath) + '/' or '')
+                rel_prefix = (
+                    (dst_is_existing_dir and not src_path.endswith('/')) and
+                    ubasename(src_npath) + '/' or '')
                 if rel_prefix:
                     d = os.path.join(dst_path, rel_prefix)
                     if not opts.dry_run:
                         log.info("mkdir %s", d)
                         os.mkdir(d)
                 for dirpath, dirents, objents in self.client.walk(src_npath):
-                    #pprint((dirpath, dirents, objents))
-                    reldirpath = os.path.normpath(
-                        rel_prefix + dirpath[len(src_npath)+1:])
+                    reldirpath = os.path.normpath(rel_prefix + dirpath[len(
+                        src_npath) + 1:])
                     for objent in objents:
-                        get_file(ujoin(dirpath, objent["name"]),
-                            os.path.normpath(os.path.join(
-                                dst_path, reldirpath, objent["name"])))
+                        get_file(
+                            ujoin(dirpath, objent["name"]),
+                            os.path.normpath(os.path.join(dst_path, reldirpath,
+                                                          objent["name"])))
                     for dirent in dirents:
                         if not opts.dry_run:
                             d = os.path.normpath(os.path.join(
@@ -1343,19 +1451,26 @@ class Mantash(cmdln.Cmdln):
                 continue
             else:
                 log.error("%s (manta path) is of unknown type, not an object "
-                    "or directory (not copied)", src_path)
+                          "or directory (not copied)", src_path)
                 retval = 1
                 continue
         return retval
 
-    @cmdln.option("-v", "--verbose", action="store_true",
-        help="show files as they are being removed")
-    @cmdln.option("-f", "--force", action="store_true",
-        help="force removal, don't complain about file not existing")
-    @cmdln.option("-r", "--recursive", action="store_true",
-        help="recursively delete a directory")
-    @cmdln.option("--dry-run", action="store_true",
-        help="do a dry-run, implies '--verbose'")
+    @cmdln.option("-v",
+                  "--verbose",
+                  action="store_true",
+                  help="show files as they are being removed")
+    @cmdln.option("-f",
+                  "--force",
+                  action="store_true",
+                  help="force removal, don't complain about file not existing")
+    @cmdln.option("-r",
+                  "--recursive",
+                  action="store_true",
+                  help="recursively delete a directory")
+    @cmdln.option("--dry-run",
+                  action="store_true",
+                  help="do a dry-run, implies '--verbose'")
     def do_rm(self, subcmd, opts, *paths):
         """rm a file from manta
 
@@ -1387,9 +1502,9 @@ class Mantash(cmdln.Cmdln):
         for path in paths:
             npath = unormpath(ujoin(self.cwd, uexpanduser(path, self.home)))
             d, b = usplit(npath)
-            if '*' in b or '?' in b or '[' in b: # we have a glob
+            if '*' in b or '?' in b or '[' in b:  # we have a glob
                 candidates = self.client.ls(d)
-                for match in fnmatch.filter(candidates.keys(), b):
+                for match in fnmatch.filter(list(candidates), b):
                     e = candidates[match]
                     e["path"] = ujoin(d, match)
                     dirents.append(e)
@@ -1417,14 +1532,22 @@ class Mantash(cmdln.Cmdln):
 
         return retval
 
-    @cmdln.option("-v", "--verbose",
-        dest="verbose", action="store_true", default=True,
+    @cmdln.option(
+        "-v",
+        "--verbose",
+        dest="verbose",
+        action="store_true",
+        default=True,
         help="show files as they are being moved. This is the default.")
-    @cmdln.option("-q", "--quiet",
-        dest="verbose", action="store_false",
-        help="show files as they are being moved")
-    @cmdln.option("-f", "--force", action="store_true",
-        help="do not stop on overwriting a target file")
+    @cmdln.option("-q",
+                  "--quiet",
+                  dest="verbose",
+                  action="store_false",
+                  help="show files as they are being moved")
+    @cmdln.option("-f",
+                  "--force",
+                  action="store_true",
+                  help="do not stop on overwriting a target file")
     @cmdln.option("--dry-run", action="store_true", help="do a dry-run")
     def do_mv(self, subcmd, opts, *paths):
         """move file(s)/dir(s) in manta
@@ -1438,6 +1561,7 @@ class Mantash(cmdln.Cmdln):
         Note: The eventual goal is to behave like a typical `mv` but for now
         this will error out if it hits target files in the way.
         """
+
         def move_dir(a, b):
             if opts.verbose:
                 log.info("mkdir -p %s", b)
@@ -1473,22 +1597,24 @@ class Mantash(cmdln.Cmdln):
         if dst_type is not None:
             # TODO: support this eventually
             log.error("target %s exists: this is not yet supported by "
-                "the mv command", dst)
+                      "the mv command", dst)
             return 1
 
         if len(srcs) > 1 and dst_type != "directory":
-            log.error("incorrect usage, destination %s is not a directory", dst)
+            log.error("incorrect usage, destination %s is not a directory",
+                      dst)
             return 1
 
         try:
-            prepend = (dst_type == "directory") #TODO: use this
+            prepend = (dst_type == "directory")  #TODO: use this
             for src in srcs:
                 nsrc = unormpath(ujoin(self.cwd, uexpanduser(src, self.home)))
                 src_stat = self.client.stat(nsrc)
                 if src_stat["type"] != "directory":
                     move_obj(nsrc, ndst)
                 else:
-                    for dirpath, dirents, objents in self.client.walk(nsrc, False):
+                    for dirpath, dirents, objents in self.client.walk(nsrc,
+                                                                      False):
                         #pprint((dirpath, dirents, objents))
                         subpath = dirpath[len(nsrc) + 1:]
                         move_dir(dirpath, ujoin(ndst, subpath))
@@ -1502,17 +1628,26 @@ class Mantash(cmdln.Cmdln):
             log.error(ex)
             return 1
 
-    @cmdln.option("-v", "--verbose",
-        dest="verbose", action="store_true", default=True,
+    @cmdln.option(
+        "-v",
+        "--verbose",
+        dest="verbose",
+        action="store_true",
+        default=True,
         help="List files as they are being copied. This is the default.")
-    @cmdln.option("-q", "--quiet",
-        dest="verbose", action="store_false",
-        help="Do not list files as they are being copied.")
-    @cmdln.option("-R", dest="recursive", action="store_true",
+    @cmdln.option("-q",
+                  "--quiet",
+                  dest="verbose",
+                  action="store_false",
+                  help="Do not list files as they are being copied.")
+    @cmdln.option(
+        "-R",
+        dest="recursive",
+        action="store_true",
         help="If SOURCE-MFILE is a directory, copy the directory and the "
-            "entire subtree. If SOURCE-MFILE ends in a '/' and TARGET-MDIR "
-            "exists, the contents of the directory are copied, rather than "
-            "the directory itself.")
+        "entire subtree. If SOURCE-MFILE ends in a '/' and TARGET-MDIR "
+        "exists, the contents of the directory are copied, rather than "
+        "the directory itself.")
     @cmdln.option("--dry-run", action="store_true", help="Do a dry-run.")
     def do_cp(self, subcmd, opts, *paths):
         """Copy files and dirs in Manta.
@@ -1523,6 +1658,7 @@ class Mantash(cmdln.Cmdln):
 
         ${cmd_option_list}
         """
+
         def cp_dir(a, b):
             if opts.verbose:
                 sys.stdout.write("%s -> %s\n" % (a, b))
@@ -1549,12 +1685,13 @@ class Mantash(cmdln.Cmdln):
             dst_type = None
 
         if len(srcs) > 1 and dst_type != "directory":
-            log.error("incorrect usage: destination %s is not a directory", dst)
+            log.error("incorrect usage: destination %s is not a directory",
+                      dst)
             return 1
 
         numWarns = 0
         try:
-            prepend = (dst_type == "directory") #TODO: use this
+            prepend = (dst_type == "directory")  #TODO: use this
             for src in srcs:
                 nsrc = unormpath(ujoin(self.cwd, uexpanduser(src, self.home)))
                 try:
@@ -1585,7 +1722,7 @@ class Mantash(cmdln.Cmdln):
                     #   cp -R adir/ bdir        # recursive cp adir bdir
                     if not opts.recursive:
                         sys.stderr.write("cp: %s is a directory "
-                            "(not copied, use '-R')\n" % src)
+                                         "(not copied, use '-R')\n" % src)
                         numWarns += 1
                         continue
                     elif dst_type not in (None, "directory"):
@@ -1599,8 +1736,8 @@ class Mantash(cmdln.Cmdln):
                         # and then "afile.txt" has the contents of the last
                         # thing copied to it. This is at least how `cp` on my
                         # Mac behaves.
-                        sys.stderr.write("cp: %s: Not a Manta directory\n"
-                            % dst)
+                        sys.stderr.write("cp: %s: Not a Manta directory\n" %
+                                         dst)
                         numWarns += 1
                         continue
 
@@ -1619,8 +1756,8 @@ class Mantash(cmdln.Cmdln):
                             subpath = srcpath[len(nsrc) + 1:]
                             cp_obj(srcpath, ujoin(ndst_base, subpath))
                 else:
-                    raise manta.MantaError("unknown manta type for %s: %s"
-                        % (nsrc, src_type))
+                    raise manta.MantaError("unknown manta type for %s: %s" %
+                                           (nsrc, src_type))
         except manta.MantaError:
             _, ex, _ = sys.exc_info()
             log.error(ex)
@@ -1637,8 +1774,10 @@ class Mantash(cmdln.Cmdln):
         """
         return unormpath(ujoin(self.cwd, uexpanduser(mpath, self.home)))
 
-    @cmdln.option("-v", "--verbose", action="store_true",
-        help="show link details as it is being linked")
+    @cmdln.option("-v",
+                  "--verbose",
+                  action="store_true",
+                  help="show link details as it is being linked")
     def do_ln(self, subcmd, opts, object_path, *link_path_args):
         """Create a snaplink to an object.
 
@@ -1662,8 +1801,8 @@ class Mantash(cmdln.Cmdln):
         elif len(link_path_args) == 0:
             link_path = unormpath(ujoin('.', ubasename(object_path)))
         else:
-            raise cmdln.CmdlnUserError("too many arguments: %s %s"
-                % (object_path, ' '.join(link_path_args)))
+            raise cmdln.CmdlnUserError("too many arguments: %s %s" %
+                                       (object_path, ' '.join(link_path_args)))
         object_npath = self._realpath(object_path)
         link_npath = self._realpath(link_path)
         if opts.verbose:
@@ -1685,14 +1824,16 @@ class Mantash(cmdln.Cmdln):
                             'o' (for objects) or 'f' (for objects).
             -ls             Print the long `ls` output for each entry.
         """
+
         class Options:
             def __init__(self, *opts):
                 for name in opts:
                     setattr(self, name, None)
+
             def __repr__(self):
-                return "<Options: %s>" % dict(
-                    (o, getattr(opts, o)) for o in dir(opts)
-                      if not o.startswith('__'))
+                return "<Options: %s>" % dict((o, getattr(opts, o))
+                                              for o in dir(opts)
+                                              if not o.startswith('__'))
 
         # Parse DIRS.
         argv.pop(0)  # "find"
@@ -1743,7 +1884,8 @@ class Mantash(cmdln.Cmdln):
                 log.error("%s: no such remote directory", d)
             elif dirents[base]["type"] == "directory":
                 for dirpath, dirents, objents in self.client.walk(d):
-                    yield {"type": "directory", "name": ubasename(dirpath),
+                    yield {"type": "directory",
+                           "name": ubasename(dirpath),
                            "path": dirpath}
                     for objent in sorted(objents, key=itemgetter("name")):
                         objent["path"] = ujoin(dirpath, objent["name"])
@@ -1783,14 +1925,14 @@ class Mantash(cmdln.Cmdln):
         npath = self._realpath(path)
         base = ubasename(npath)
         local_path = os.path.join(CACHE_DIR, "vi",
-            "%s-local.%s" % (os.getpid(), base))
+                                  "%s-local.%s" % (os.getpid(), base))
         if not os.path.exists(os.path.dirname(local_path)):
             os.makedirs(os.path.dirname(local_path))
 
         try:
             res, _ = self.client.get_object2(npath, path=local_path)
             is_new = False
-            old_digest = res["content-md5"].decode("base64")
+            old_digest = base64.b64decode(res["content-md5"])
             content_type = res["content-type"]
         except manta.MantaAPIError:
             _, ex, _ = sys.exc_info()
@@ -1807,7 +1949,7 @@ class Mantash(cmdln.Cmdln):
 
         os.system('vi -f "%s"' % local_path)
 
-        f = open(local_path, 'rb') # TODO: would md5 comparison be preferable?
+        f = open(local_path, 'rb')  # TODO: would md5 comparison be preferable?
         new_digest = md5(f.read()).digest()
         f.close()
         if new_digest == old_digest:
@@ -1816,11 +1958,16 @@ class Mantash(cmdln.Cmdln):
         else:
             self.client.put(npath, path=local_path, content_type=content_type)
             log.info("'%s' saved.", path)
+
     do_vi.aliases = ["vim"]
 
-    @cmdln.option("-j", "--json", action="store_true",
-        help="display the jobs in JSON")
-    @cmdln.option("-s", "--state",
+    @cmdln.option("-j",
+                  "--json",
+                  action="store_true",
+                  help="display the jobs in JSON")
+    @cmdln.option(
+        "-s",
+        "--state",
         help="limit to jobs in the given state: 'running', 'done', etc.")
     def do_jobs(self, subcmd, opts):
         """List Manta jobs
@@ -1838,7 +1985,7 @@ class Mantash(cmdln.Cmdln):
             print(json.dumps(jobs, indent=2))
         else:
             for job in jobs:
-                print job['name']
+                print(job['name'])
 
     def do_jobinfo(self, subcmd, opts, job_id):
         """Get details for a Manta job.
@@ -1904,8 +2051,8 @@ class Mantash(cmdln.Cmdln):
                 return 0
             pipe_chars_str = "', '".join(pipe_chars[:-1]) \
                 + "' or '%s" % pipe_chars[-1]
-            raise MantashError("no '%s' pipe char found in job request"
-                % pipe_chars_str)
+            raise MantashError("no '%s' pipe char found in job request" %
+                               pipe_chars_str)
         pipe_idx = min(pipe_idxs)
         first_argv = argv[:pipe_idx]
         rest_argv = argv[pipe_idx:]
@@ -1951,8 +2098,8 @@ class Mantash(cmdln.Cmdln):
                     for pname, dirent in dirents.items():
                         if dirent["type"] != "object":
                             raise MantashError(
-                                "'%s' is a %s (can only process objects)" % (
-                                pname, dirent["type"]))
+                                "'%s' is a %s (can only process objects)" %
+                                (pname, dirent["type"]))
                         keys.append(unormpath(ujoin(self.cwd, pname)))
 
         if DEBUG:
@@ -1968,7 +2115,8 @@ class Mantash(cmdln.Cmdln):
         self.client.end_job_input(job_id)
         #TODO: support cancel_job on ^C
         if opts.verbose:
-            sys.stderr.write("Waiting for job %s to complete\n" % job_id)  #TODO log?
+            sys.stderr.write("Waiting for job %s to complete\n" %
+                             job_id)  #TODO log?
         self._wait_for_job(job_id, timeout=opts.timeout)
         outkeys = self.client.get_job_output(job_id)
         for outkey in outkeys:
@@ -2021,9 +2169,12 @@ class Mantash(cmdln.Cmdln):
                 # Last arg is an existing manta path.
                 args[-1] = candidate
         opts = [
-            '-u', self.manta_url,
-            '-k', self.options.key_id,
-            '-a', self.account,
+            '-u',
+            self.manta_url,
+            '-k',
+            self.options.key_id,
+            '-a',
+            self.account,
         ]
         if self.options.insecure:
             opts += ['-i']
@@ -2041,7 +2192,7 @@ class Mantash(cmdln.Cmdln):
         else:
             stdout, stderr = p.communicate()
             p.wait()
-            return self.do_login.__doc__ + '\n\n' + stdout
+            return self.do_login.__doc__ + '\n\n' + stdout.decode('utf-8')
 
     def do_sign(self, argv):
         """Produce a signed URL for the given manta path.
@@ -2064,9 +2215,12 @@ class Mantash(cmdln.Cmdln):
                 # Last arg is an existing manta path.
                 args[-1] = candidate
         opts = [
-            '-u', self.manta_url,
-            '-k', self.options.key_id,
-            '-a', self.account,
+            '-u',
+            self.manta_url,
+            '-k',
+            self.options.key_id,
+            '-a',
+            self.account,
         ]
         if self.options.insecure:
             opts += ['-i']
@@ -2084,12 +2238,11 @@ class Mantash(cmdln.Cmdln):
         else:
             stdout, stderr = p.communicate()
             p.wait()
-            return self.do_sign.__doc__ + '\n\n' + stdout
-
-
+            return self.do_sign.__doc__ + '\n\n' + stdout.decode("utf-8")
 
 #---- internal support stuff
 
+
 def uexpanduser(mpath, home):
     """Like os.path.expanduser for mpaths."""
     if not mpath or not mpath.startswith('~'):
@@ -2128,7 +2281,7 @@ def argv2line(argv):
     for arg in argv:
         if ' ' in arg or '\t' in arg:
             arg = arg.replace("'", r"\'")
-            arg = "'"+arg+"'"
+            arg = "'" + arg + "'"
         escapedArgs.append(arg)
     return ' '.join(escapedArgs)
 
@@ -2136,17 +2289,15 @@ def argv2line(argv):
 def _indent(s, indent='    '):
     return indent + indent.join(s.splitlines(True))
 
-
-
 #---- mainline
 
+
 def main(argv=sys.argv):
     logging.basicConfig(format='%(name)s: %(levelname)s: %(message)s')
-    log.setLevel(logging.INFO)
+    log.setLevel(logging.ERROR)
     shell = Mantash()
     return shell.main(argv, loop=cmdln.LOOP_IF_EMPTY)
 
-
 ## {{{ http://code.activestate.com/recipes/577258/ (r5)
 if __name__ == "__main__":
     try:
@@ -2168,13 +2319,12 @@ if __name__ == "__main__":
                 skip_it = True
             if not skip_it:
                 tb_path, tb_lineno, tb_func = traceback.extract_tb(tb)[-1][:3]
-                log.error("%s (%s:%s in %s)", exc_info[1], tb_path,
-                    tb_lineno, tb_func)
+                log.error("%s (%s:%s in %s)", exc_info[1], tb_path, tb_lineno,
+                          tb_func)
         else:  # string exception
             log.error(exc_info[0])
         if not skip_it:
             if True or log.isEnabledFor(logging.DEBUG):
-                print('')
                 traceback.print_exception(*exc_info)
             sys.exit(1)
     else:
diff --git a/manta/__init__.py b/manta/__init__.py
index 0013ac9..98c29d7 100644
--- a/manta/__init__.py
+++ b/manta/__init__.py
@@ -2,6 +2,7 @@
 
 """A Python client/CLI/shell/SDK for Joyent Manta."""
 
+from __future__ import absolute_import
 from .version import __version__
 from .client import MantaClient
 from .auth import PrivateKeySigner, SSHAgentSigner, CLISigner
diff --git a/manta/appdirs.py b/manta/appdirs.py
index 94c592f..9e70530 100644
--- a/manta/appdirs.py
+++ b/manta/appdirs.py
@@ -11,10 +11,12 @@
 # - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
 # - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
 
+from __future__ import absolute_import
+from __future__ import print_function
+
 __version_info__ = (1, 2, 0)
 __version__ = '.'.join(map(str, __version_info__))
 
-
 import sys
 import os
 
diff --git a/manta/auth.py b/manta/auth.py
index 7a2aa46..42e89ef 100644
--- a/manta/auth.py
+++ b/manta/auth.py
@@ -2,8 +2,10 @@
 
 """Manta client auth."""
 
+from __future__ import absolute_import
 import binascii
 import sys
+import io
 import os
 from os.path import expanduser
 import logging
@@ -57,6 +59,7 @@
 
 #---- internal support stuff
 
+
 def fingerprint_from_ssh_pub_key(data):
     """Calculate the fingerprint of SSH public key data.
 
@@ -80,7 +83,8 @@ def fingerprint_from_ssh_pub_key(data):
 
     key = base64.b64decode(data)
     fp_plain = hashlib.md5(key).hexdigest()
-    return ':'.join(a+b for a,b in zip(fp_plain[::2], fp_plain[1::2]))
+    return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
+
 
 def fingerprint_from_raw_ssh_pub_key(key):
     """Encode a raw SSH key (string of bytes, as from
@@ -88,7 +92,7 @@ def fingerprint_from_raw_ssh_pub_key(key):
     '54:c7:4c:93:cf:ff:e3:32:68:bc:89:6e:5e:22:b5:9c' form.
     """
     fp_plain = hashlib.md5(key).hexdigest()
-    return ':'.join(a+b for a,b in zip(fp_plain[::2], fp_plain[1::2]))
+    return ':'.join(a + b for a, b in zip(fp_plain[::2], fp_plain[1::2]))
 
 
 def sha256_fingerprint_from_ssh_pub_key(data):
@@ -101,15 +105,16 @@ def sha256_fingerprint_from_ssh_pub_key(data):
         data = data.split(None, 2)[1]
 
     digest = hashlib.sha256(binascii.a2b_base64(data)).digest()
-    encoded = base64.b64encode(digest).rstrip('=') # ssh-keygen strips this
-    return 'SHA256:' + encoded
+    encoded = base64.b64encode(digest)  # ssh-keygen strips thi
+    return 'SHA256:' + encoded.decode('utf-8')
 
 def sha256_fingerprint_from_raw_ssh_pub_key(raw_key):
     """Encode a raw SSH key (string of bytes, as from
     `str(paramiko.AgentKey)`) to a fingerprint in the SHA256 form:
         SHA256:j2WoSeOWhFy69BQ39fuafFAySp9qCZTSCEyT2vRKcL+s
     """
-    h = hashlib.sha256(raw_key).digest().encode('base64')
+    digest = hashlib.sha256(raw_key).digest()
+    h = base64.b64encode(digest).decode('utf-8')
     h = h.rstrip().rstrip('=')  # drop newline and possible base64 padding
     return 'SHA256:' + h
 
@@ -136,13 +141,13 @@ def load_ssh_key(key_id, skip_priv_key=False):
     # If `key_id` is already a private key path, then easy.
     if not FINGERPRINT_RE.match(key_id):
         if not skip_priv_key:
-            f = open(key_id)
+            f = io.open(key_id, 'rb')
             try:
                 priv_key = f.read()
             finally:
                 f.close()
         pub_key_path = key_id + '.pub'
-        f = open(pub_key_path)
+        f = io.open(pub_key_path, 'r')
         try:
             pub_key = f.read()
         finally:
@@ -169,8 +174,8 @@ def load_ssh_key(key_id, skip_priv_key=False):
     pub_key = None
     for pub_key_path in glob(pub_key_glob):
         try:
-            f = open(pub_key_path)
-        except IOError, ex:
+            f = io.open(pub_key_path, 'r')
+        except IOError:
             # This can happen if the .pub file is a broken symlink.
             log.debug("could not open '%s', skip it", pub_key_path)
             continue
@@ -207,7 +212,7 @@ def load_ssh_key(key_id, skip_priv_key=False):
 
     priv_key_path = os.path.splitext(pub_key_path)[0]
     if not skip_priv_key:
-        f = open(priv_key_path)
+        f = io.open(priv_key_path, 'rb')
         try:
             priv_key = f.read()
         finally:
@@ -253,10 +258,7 @@ def ssh_key_info_from_key_data(key_id, priv_key=None):
         - ... some others added by `load_ssh_key()`
     """
     if FINGERPRINT_RE.match(key_id) and priv_key:
-        key_info = {
-            "fingerprint": key_id,
-            "priv_key": priv_key
-        }
+        key_info = {"fingerprint": key_id, "priv_key": priv_key}
     else:
         # Otherwise, we attempt to load necessary details from ~/.ssh.
         key_info = load_ssh_key(key_id)
@@ -268,7 +270,7 @@ def ssh_key_info_from_key_data(key_id, priv_key=None):
             key_info["priv_key"],
             password=None,
             backend=default_backend())
-    except TypeError, ex:
+    except TypeError as ex:
         log.debug("could not import key without passphrase (will "
             "try with passphrase): %s", ex)
         if "priv_key_path" in key_info:
@@ -295,7 +297,7 @@ def ssh_key_info_from_key_data(key_id, priv_key=None):
             raise MantaError("could not import key" + details)
 
     # If load_ssh_key() wasn't run, set the algorithm here.
-    if not key_info.has_key('algorithm'):
+    if 'algorithm' not in key_info:
         if isinstance(key, ec.EllipticCurvePrivateKey):
             key_info['algorithm'] = ECDSA_ALGO_FROM_KEY_SIZE[str(key.key_size)]
         elif isinstance(key, rsa.RSAPrivateKey):
@@ -307,7 +309,6 @@ def ssh_key_info_from_key_data(key_id, priv_key=None):
     key_info["type"] = "ssh_key"
     return key_info
 
-
 def agent_key_info_from_key_id(key_id):
     """Find a matching key in the ssh-agent.
 
@@ -332,7 +333,8 @@ def agent_key_info_from_key_id(key_id):
     keys = Agent().get_keys()
 
     for key in keys:
-        raw_key = str(key)
+        raw_key = key.blob
+
 
         # The MD5 fingerprint functions return the hexdigest without the hash
         # algorithm prefix ("MD5:"), and the SHA256 functions return the
@@ -349,8 +351,8 @@ def agent_key_info_from_key_id(key_id):
             md5_fingerprint = md5_fp
             break
     else:
-        raise MantaError(
-            'no ssh-agent key with fingerprint "%s"' % fingerprint)
+        raise MantaError('no ssh-agent key with fingerprint "%s"' %
+                         fingerprint)
 
     return {
         "type": "agent",
@@ -359,7 +361,6 @@ def agent_key_info_from_key_id(key_id):
         "algorithm": ALGO_FROM_SSH_KEY_TYPE[key.name]
     }
 
-
 def ssh_key_sign(key_info, message):
     algo = key_info["algorithm"].split('-')
     hash_algo = algo[1]
@@ -372,6 +373,8 @@ def ssh_key_sign(key_info, message):
         "sha512": SHA512
     }[hash_algo]
 
+    assert isinstance(message, bytes)
+
     if key_type == 'ecdsa':
         signed_raw = key_info["signer"].sign(
             message,
@@ -390,8 +393,10 @@ def ssh_key_sign(key_info, message):
 
 #---- exports
 
+
 class Signer(object):
     """A virtual base class for python-manta request signing."""
+
     def sign(self, s):
         """Sign the given string.
 
@@ -402,6 +407,7 @@ def sign(self, s):
         """
         raise NotImplementedError("this is a virtual base class")
 
+
 class PrivateKeySigner(Signer):
     """Sign Manta requests with the given ssh private key.
 
@@ -415,20 +421,24 @@ class PrivateKeySigner(Signer):
     to load required key data (both public and private key files) from
     keys in "~/.ssh/".
     """
+
     def __init__(self, key_id, priv_key=None):
         self.key_id = key_id
         self.priv_key = priv_key
 
     _key_info_cache = None
+
     def _get_key_info(self):
         """Get key info appropriate for signing."""
         if self._key_info_cache is None:
-            self._key_info_cache = ssh_key_info_from_key_data(
-                self.key_id, self.priv_key)
+            self._key_info_cache = ssh_key_info_from_key_data(self.key_id,
+                                                              self.priv_key)
         return self._key_info_cache
 
     def sign(self, s):
-        assert isinstance(s, str)   # for now, not unicode. Python 3?
+        if not isinstance(s, bytes):
+            assert isinstance(s, str)
+            s = s.encode("utf-8")
 
         key_info = self._get_key_info()
 
@@ -438,6 +448,7 @@ def sign(self, s):
 
         return (key_info["algorithm"], key_info["fingerprint"], signed)
 
+
 class SSHAgentSigner(Signer):
     """Sign Manta requests using an ssh-agent.
 
@@ -445,10 +456,12 @@ class SSHAgentSigner(Signer):
         'b3:f0:a1:6c:18:3b:42:63:fd:6e:57:42:74:17:d4:bc', or the path to
         an ssh private key file (like ssh's IdentityFile config option).
     """
+
     def __init__(self, key_id):
         self.key_id = key_id
 
     _key_info_cache = None
+
     def _get_key_info(self):
         """Get key info appropriate for signing."""
         if self._key_info_cache is None:
@@ -456,7 +469,10 @@ def _get_key_info(self):
         return self._key_info_cache
 
     def sign(self, s):
-        assert isinstance(s, str)   # for now, not unicode. Python 3?
+
+        if not isinstance(s, bytes):
+            assert isinstance(s, str)
+            s = s.encode("utf-8")
 
         key_info = self._get_key_info()
         assert key_info["type"] == "agent"
@@ -472,14 +488,17 @@ def sign(self, s):
 
         return (key_info["algorithm"], key_info["fingerprint"], signed)
 
+
 class CLISigner(Signer):
     """Sign Manta requests using the SSH agent (if available and has the
     required key) or loading keys from "~/.ssh/*".
     """
+
     def __init__(self, key_id):
         self.key_id = key_id
 
     _key_info_cache = None
+
     def _get_key_info(self):
         """Get key info appropriate for signing: either from the ssh agent
         or from a private key.
@@ -509,15 +528,18 @@ def _get_key_info(self):
             self._key_info_cache = key_info
             return self._key_info_cache
 
-        raise MantaError("could not find key info for signing: %s"
-            % "; ".join(map(unicode, errors)))
+        raise MantaError("could not find key info for signing: %s" %
+                         "; ".join(map(str, errors)))
 
     def sign(self, sigstr):
-        assert isinstance(sigstr, str)   # for now, not unicode. Python 3?
+        if not isinstance(sigstr, bytes):
+            assert isinstance(sigstr, str)
+            sigstr = sigstr.encode("utf-8")
 
         key_info = self._get_key_info()
         log.debug("sign %r with %s key (algo %s, fp %s)", sigstr,
-            key_info["type"], key_info["algorithm"], key_info["fingerprint"])
+                  key_info["type"], key_info["algorithm"],
+                  key_info["fingerprint"])
 
         if key_info["type"] == "agent":
             response = key_info["agent_key"].sign_ssh_data(sigstr)
@@ -531,7 +553,7 @@ def sign(self, sigstr):
         elif key_info["type"] == "ssh_key":
             signed = ssh_key_sign(key_info, sigstr)
         else:
-            raise MantaError("internal error: unknown key type: %r"
-                % key_info["type"])
+            raise MantaError("internal error: unknown key type: %r" %
+                             key_info["type"])
 
         return (key_info["algorithm"], key_info["fingerprint"], signed)
diff --git a/manta/client.py b/manta/client.py
index c4657b4..531b2a0 100644
--- a/manta/client.py
+++ b/manta/client.py
@@ -1,9 +1,10 @@
 # Copyright (c) 2016 Joyent, Inc.  All rights reserved.
-
 """The Manta client."""
 
+from __future__ import absolute_import
 import sys
 import logging
+import io
 import os
 from os.path import exists
 from posixpath import join as ujoin, dirname as udirname, basename as ubasename
@@ -20,8 +21,6 @@
 
 import httplib2
 
-
-
 #---- Python version compat
 
 try:
@@ -33,8 +32,6 @@
     from urllib import urlencode
     from urllib import quote as urlquote
 
-
-
 #---- globals
 
 log = logging.getLogger("manta.client")
@@ -42,34 +39,15 @@
 # Use a separate cache dir tree if running with a different UID (e.g. if
 # acting as root via `sudo -E`). This avoids cache dir permission
 # issues. See <https://github.com/joyent/python-manta/issues/20>.
-DEFAULT_HTTP_CACHE_DIR = appdirs.user_cache_dir(
-    "python-manta-%s" % os.geteuid(), "Joyent", "http")
+DEFAULT_HTTP_CACHE_DIR = appdirs.user_cache_dir("python-manta-%s" %
+                                                os.geteuid(), "Joyent", "http")
 
 DEFAULT_USER_AGENT = "python-manta/%s (%s) Python/%s" % (
     __version__, sys.platform, sys.version.split(None, 1)[0])
 
-
-#---- compat
-
-# Python version compat
-# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3).
-if sys.version_info[0] <= 2:
-    py3 = False
-    try:
-        bytes
-    except NameError:
-        bytes = str
-    base_string_type = basestring
-elif sys.version_info[0] >= 3:
-    py3 = True
-    unicode = str
-    base_string_type = str
-    unichr = chr
-
-
-
 #---- internal support stuff
 
+
 def http_date(d=None):
     """Return HTTP Date format string for the given date.
     http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
@@ -84,33 +62,41 @@ def http_date(d=None):
 def _indent(s, indent='    '):
     return indent + indent.join(s.splitlines(True))
 
+
 class MantaHttp(httplib2.Http):
-    def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey):
+    def _request(self, conn, host, absolute_uri, request_uri, method, body,
+                 headers, redirections, cachekey):
         if log.isEnabledFor(logging.DEBUG):
-            body_str = body or '(none)'
-            if body and len(body) > 1024:
-                body_str = body[:1021] + '...'
-            log.debug("req: %s %s\n%s", method, request_uri,
-                '\n'.join([
-                    _indent("host: " + host),
-                    _indent("headers: " + pformat(headers)),
-                    #_indent("cachekey: " + pformat(cachekey)), #XXX
-                    _indent("body: " + body_str)
-                ]))
-        res, content = httplib2.Http._request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey)
+            if body is not None:
+                body_str = body.decode('utf-8', 'backslashreplace')
+            else:
+                body_str = '(none)'
+            if len(body_str) > 1024:
+                body_str = body_str[:1021] + '...'
+            log.debug("req: %s %s\n%s",
+                      method,
+                      request_uri,
+                      '\n'.join([
+                          _indent("host: " + host),
+                          _indent("headers: " + pformat(headers)),
+                          #_indent("cachekey: " + pformat(cachekey)), #XXX
+                          _indent("body: " + body_str)
+                      ]))
+
+        if 'location' in headers:
+            headers['location'] = urlquote(headers['location'])
+        res, content = httplib2.Http._request(self, conn, host, absolute_uri,
+                                              request_uri, method, body,
+                                              headers, redirections, cachekey)
         if log.isEnabledFor(logging.DEBUG):
-            ucontent = content.decode('utf-8')
-            log.debug("res: %s %s\n%s\n%s", method, request_uri,
-                _indent(pformat(res)),
-                (len(ucontent) < 1024 and _indent(ucontent)
-                 or _indent(ucontent[:1021] + u'...')))
-        return (res, content)
-
-
+            log.debug("res: %s %s\n%s", method, request_uri,
+                      _indent(pformat(res)))
 
+        return (res, content)
 
 #---- exports
 
+
 class RawMantaClient(object):
     """A raw client for accessing the Manta REST API. Here "raw" means that
     the API is limited to the strict set of endpoints in the REST API. No
@@ -133,8 +119,16 @@ class RawMantaClient(object):
     @param verbose {bool} Optional. Default false. If true, then will log
         debugging info.
     """
-    def __init__(self, url, account, subuser=None, role=None, sign=None,
-                 signer=None, user_agent=None, cache_dir=None,
+
+    def __init__(self,
+                 url,
+                 account,
+                 subuser=None,
+                 role=None,
+                 sign=None,
+                 signer=None,
+                 user_agent=None,
+                 cache_dir=None,
                  disable_ssl_certificate_validation=False,
                  verbose=False):
         assert account, 'account'
@@ -157,15 +151,23 @@ def __init__(self, url, account, subuser=None, role=None, sign=None,
             manta.auth.log.setLevel(logging.DEBUG)
 
     _http_cache = None
+
     def _get_http(self):
         if not self._http_cache:
             if not exists(self.cache_dir):
                 os.makedirs(self.cache_dir)
-            self._http_cache = MantaHttp(self.cache_dir,
-                disable_ssl_certificate_validation=self.disable_ssl_certificate_validation)
+            self._http_cache = MantaHttp(
+                self.cache_dir,
+                disable_ssl_certificate_validation=self.
+                disable_ssl_certificate_validation)
         return self._http_cache
 
-    def _request(self, path, method="GET", query=None, body=None, headers=None):
+    def _request(self,
+                 path,
+                 method="GET",
+                 query=None,
+                 body=None,
+                 headers=None):
         """Make a Manta request
 
         ...
@@ -175,7 +177,7 @@ def _request(self, path, method="GET", query=None, body=None, headers=None):
 
         # Presuming utf-8 encoding here for requests. Not sure if that is
         # technically correct.
-        if isinstance(path, unicode):
+        if not isinstance(path, bytes):
             spath = path.encode('utf-8')
         else:
             spath = path
@@ -198,15 +200,23 @@ def _request(self, path, method="GET", query=None, body=None, headers=None):
             if "Date" not in headers:
                 headers["Date"] = http_date()
             sigstr = 'date: ' + headers["Date"]
-            algorithm, fingerprint, signature = self.signer.sign(sigstr)
+            algorithm, fingerprint, signature = self.signer.sign(sigstr.encode(
+                'utf-8'))
             auth = 'Signature keyId="/%s/keys/%s",algorithm="%s",signature="%s"'\
                    % ('/'.join(filter(None, [self.account, self.subuser])),
-                      fingerprint, algorithm, signature)
+                      fingerprint, algorithm, signature.decode('utf-8'))
+
             headers["Authorization"] = auth
 
             if self.role:
                 headers['Role'] = self.role
 
+        # python 3
+        try:
+            url = url.decode('utf-8')  # encoding='utf-8'
+        except:
+            pass
+
         return http.request(url, method, ubody, headers)
 
     def put_directory(self, mdir):
@@ -216,9 +226,7 @@ def put_directory(self, mdir):
         @param mdir {str} A manta path, e.g. '/trent/stor/mydir'.
         """
         log.debug('PutDirectory %r', mdir)
-        headers = {
-            "Content-Type": "application/json; type=directory"
-        }
+        headers = {"Content-Type": "application/json; type=directory"}
         res, content = self._request(mdir, "PUT", headers=headers)
         if res["status"] != "204":
             raise errors.MantaAPIError(res, content)
@@ -262,7 +270,7 @@ def list_directory2(self, mdir, limit=None, marker=None):
             if not line.strip():
                 continue
             try:
-                dirents.append(json.loads(line))
+                dirents.append(json.loads(line.decode("utf-8")))
             except ValueError:
                 raise errors.MantaError('invalid directory entry: %r' % line)
         return res, dirents
@@ -294,7 +302,11 @@ def delete_directory(self, mdir):
         if res["status"] != "204":
             raise errors.MantaAPIError(res, content)
 
-    def put_object(self, mpath, content=None, path=None, file=None,
+    def put_object(self,
+                   mpath,
+                   content=None,
+                   path=None,
+                   file=None,
                    content_length=None,
                    content_type="application/octet-stream",
                    durability_level=None):
@@ -321,33 +333,38 @@ def put_object(self, mpath, content=None, path=None, file=None,
             Manta the number of copies to keep.
         """
         log.debug('PutObject %r', mpath)
-        headers = {
-            "Content-Type": content_type,
-        }
+        headers = {"Content-Type": content_type, }
         if durability_level:
             headers["x-durability-level"] = durability_level
 
         methods = [m for m in [content, path, file] if m is not None]
         if len(methods) != 1:
             raise errors.MantaError("exactly one of 'content', 'path' or "
-                "'file' must be provided")
+                                    "'file' must be provided")
         if content is not None:
             pass
         elif path:
-            f = open(path)
+            f = io.open(path, 'rb')
             try:
                 content = f.read()
             finally:
                 f.close()
         else:
             content = file.read()
-        if not isinstance(content, bytes):
-            raise errors.MantaError("'content' must be bytes, not unicode")
+
+        try:
+            # python 3
+            content_bytes = bytes(content, encoding='utf-8')
+        except:
+            # python 2
+            content_bytes = content
 
         headers["Content-Length"] = str(len(content))
-        md5 = hashlib.md5(content)
+        md5 = hashlib.md5(content_bytes)
         headers["Content-MD5"] = base64.b64encode(md5.digest())
-        res, content = self._request(mpath, "PUT", body=content,
+        res, content = self._request(mpath,
+                                     "PUT",
+                                     body=content,
                                      headers=headers)
         if res["status"] != "204":
             raise errors.MantaAPIError(res, content)
@@ -366,7 +383,13 @@ def get_object(self, mpath, path=None, accept="*/*"):
             content.
         """
         res, content = self.get_object2(mpath, path=path, accept=accept)
-        return content
+        try:
+            if isinstance(content, bytes):
+                return content.decode(sys.stdout.encoding)
+            else:
+                return content
+        except UnicodeDecodeError:
+            return content
 
     def get_object2(self, mpath, path=None, accept="*/*"):
         """A lower-level version of `get_object` that returns the
@@ -377,24 +400,24 @@ def get_object2(self, mpath, path=None, accept="*/*"):
             provided
         """
         log.debug('GetObject %r', mpath)
-        headers = {
-            "Accept": accept
-        }
+        headers = {"Accept": accept}
 
         res, content = self._request(mpath, "GET", headers=headers)
         if res["status"] not in ("200", "304"):
             raise errors.MantaAPIError(res, content)
         if len(content) != int(res["content-length"]):
             raise errors.MantaError("content-length mismatch: expected %d, "
-                "got %s" % (res["content-length"], content))
+                                    "got %s" %
+                                    (res["content-length"], content))
         if res.get("content-md5"):
             md5 = hashlib.md5(content)
-            content_md5 = base64.b64encode(md5.digest())
+            content_md5 = base64.b64encode(md5.digest()).decode("utf-8")
             if content_md5 != res["content-md5"]:
-                raise errors.MantaError("content-md5 mismatch: expected %d, "
-                    "got %s" % (res["content-md5"], content_md5))
+                raise errors.MantaError("content-md5 mismatch: expected %s, "
+                                        "got %s" %
+                                        (res["content-md5"], content_md5))
         if path is not None:
-            f = open(path, 'wb')
+            f = io.open(path, 'wb')
             try:
                 f.write(content)
             finally:
@@ -441,13 +464,15 @@ def create_job(self, phases, name=None, input=None):
         log.debug('CreateJob')
         path = '/%s/jobs' % self.account
         body = {"phases": phases}
-        if name: body["name"] = name
-        if input: body["input"] = input
-        headers = {
-            "Content-Type": "application/json"
-        }
-        res, content = self._request(path, "POST", body=json.dumps(body),
-            headers=headers)
+        if name:
+            body["name"] = name
+        if input:
+            body["input"] = input
+        headers = {"Content-Type": "application/json"}
+        res, content = self._request(path,
+                                     "POST",
+                                     body=json.dumps(body),
+                                     headers=headers)
         if res["status"] != '201':
             raise errors.MantaAPIError(res, content)
         location = res["location"]
@@ -489,9 +514,7 @@ def cancel_job(self, job_id):
         """
         log.debug("CancelJob %r", job_id)
         path = "/%s/jobs/%s/live/cancel" % (self.account, job_id)
-        headers = {
-            "Content-Length": "0"
-        }
+        headers = {"Content-Length": "0"}
         res, content = self._request(path, "POST", headers=headers)
         if res["status"] != "204":
             raise errors.MantaAPIError(res, content)
@@ -608,7 +631,6 @@ def _job_errors_from_content(self, content):
         return errs
 
 
-
 class MantaClient(RawMantaClient):
     """A Manta client that builds on `RawMantaClient` to provide some
     API sugar.
@@ -698,8 +720,7 @@ def ls(self, mdir, limit=None, marker=None):
         else:
             marker = None
             while True:
-                res, entries = self.list_directory2(
-                    mdir, marker=marker)
+                res, entries = self.list_directory2(mdir, marker=marker)
                 if marker:
                     entries.pop(0)  # first one is a repeat (the marker)
                 if not entries:
@@ -759,9 +780,9 @@ def mkdir(self, mdir, parents=False):
             #       i=6 -> d: /trent/stor/builds/a/b
             #       i=7 -> d: /trent/stor/builds/a/b/c
             end = len(parts) + 1
-            start = 3 # Index of the first possible dir to create.
+            start = 3  # Index of the first possible dir to create.
             while start < end - 1:
-                idx = (end - start) / 2 + start
+                idx = int((end - start) // 2 + start)
                 d = '/'.join(parts[:idx])
                 try:
                     self.put_directory(d)
@@ -793,8 +814,8 @@ def stat(self, mpath):
         if len(parts) == 0:
             raise errors.MantaError("cannot stat empty manta path: %r" % mpath)
         elif len(parts) <= 3:
-            raise errors.MantaError(
-                "cannot stat special manta path: %r" % mpath)
+            raise errors.MantaError("cannot stat special manta path: %r" %
+                                    mpath)
         mparent = udirname(mpath)
         name = ubasename(mpath)
         dirents = self.ls(mparent)
@@ -831,7 +852,7 @@ def get_job(self, job_id):
         """
         try:
             return RawMantaClient.get_job(self, job_id)
-        except errors.MantaAPIError, ex:
+        except errors.MantaAPIError as ex:
             if ex.res.status != 404:
                 raise
             # Job was archived, try to retrieve the archived data.
@@ -851,7 +872,7 @@ def get_job_input(self, job_id):
         """
         try:
             return RawMantaClient.get_job_input(self, job_id)
-        except errors.MantaAPIError, ex:
+        except errors.MantaAPIError as ex:
             if ex.res.status != 404:
                 raise
             # Job was archived, try to retrieve the archived data.
@@ -869,7 +890,7 @@ def get_job_output(self, job_id):
         """
         try:
             return RawMantaClient.get_job_output(self, job_id)
-        except errors.MantaAPIError, ex:
+        except errors.MantaAPIError as ex:
             if ex.res.status != 404:
                 raise
             # Job was archived, try to retrieve the archived data.
@@ -887,7 +908,7 @@ def get_job_failures(self, job_id):
         """
         try:
             return RawMantaClient.get_job_failures(self, job_id)
-        except errors.MantaAPIError, ex:
+        except errors.MantaAPIError as ex:
             if ex.res.status != 404:
                 raise
             # Job was archived, try to retrieve the archived data.
@@ -905,7 +926,7 @@ def get_job_errors(self, job_id):
         """
         try:
             return RawMantaClient.get_job_errors(self, job_id)
-        except errors.MantaAPIError, ex:
+        except errors.MantaAPIError as ex:
             if ex.res.status != 404:
                 raise
             # Job was archived, try to retrieve the archived data.
diff --git a/manta/cmdln.py b/manta/cmdln.py
index 1b4e377..79472ad 100644
--- a/manta/cmdln.py
+++ b/manta/cmdln.py
@@ -3,7 +3,6 @@
 # Copyright (c) 2002-2009 ActiveState Software Inc.
 # License: MIT (see LICENSE.txt for license details)
 # Author:  Trent Mick
-
 """An improvement on Python's standard cmd.py module.
 
 As with cmd.py, this module provides "a simple framework for writing
@@ -35,7 +34,10 @@ def do_status(self, subcmd, opts, *paths):
 details.
 """
 
-__version_info__ = (1, 2, 1)
+from __future__ import absolute_import
+from __future__ import print_function
+
+__version_info__ = (2, 0, 0)
 __version__ = '.'.join(map(str, __version_info__))
 
 import os
@@ -47,8 +49,6 @@ def do_status(self, subcmd, opts, *paths):
 import sys
 import datetime
 
-
-
 #---- globals
 
 LOOP_ALWAYS, LOOP_NEVER, LOOP_IF_EMPTY = range(3)
@@ -56,31 +56,30 @@ def do_status(self, subcmd, opts, *paths):
 # An unspecified optional argument when None is a meaningful value.
 _NOT_SPECIFIED = ("Not", "Specified")
 
-# Pattern to match a TypeError message from a call that
-# failed because of incorrect number of arguments (see
-# Python/getargs.c).
-_INCORRECT_NUM_ARGS_RE = re.compile(
-    r"(takes [\w ]+ )(\d+)( arguments? \()(\d+)( given\))")
-
-
+PY3 = (sys.version_info[0] >= 3)
+if not PY3:
+    input = raw_input
 
 #---- exceptions
 
+
 class CmdlnError(Exception):
     """A cmdln.py usage error."""
+
     def __init__(self, msg):
         self.msg = msg
+
     def __str__(self):
         return self.msg
 
+
 class CmdlnUserError(Exception):
     """An error by a user of a cmdln-based tool/shell."""
     pass
 
-
-
 #---- public methods and classes
 
+
 def alias(*aliases):
     """Decorator to add aliases for Cmdln.do_* command handlers.
 
@@ -90,11 +89,13 @@ class MyShell(cmdln.Cmdln):
             def do_shell(self, argv):
                 #...implement 'shell' command
     """
+
     def decorate(f):
         if not hasattr(f, "aliases"):
             f.aliases = []
         f.aliases += aliases
         return f
+
     return decorate
 
 
@@ -119,19 +120,22 @@ def do_status(self, argv):
             retval = shell.main()
             sys.exit(retval)
     """
-    name = None      # if unset, defaults basename(sys.argv[0])
-    prompt = None    # if unset, defaults to self.name+"> "
-    version = None   # if set, default top-level options include --version
+    name = None  # if unset, defaults basename(sys.argv[0])
+    prompt = None  # if unset, defaults to self.name+"> "
+    version = None  # if set, default top-level options include --version
 
     # Default messages for some 'help' command error cases.
     # They are interpolated with one arg: the command.
     nohelp = "no help on '%s'"
     unknowncmd = "unknown command: '%s'"
 
-    helpindent = '' # string with which to indent help output
+    helpindent = ''  # string with which to indent help output
 
-    def __init__(self, completekey='tab',
-                 stdin=None, stdout=None, stderr=None):
+    def __init__(self,
+                 completekey='tab',
+                 stdin=None,
+                 stdout=None,
+                 stderr=None):
         """Cmdln(completekey='tab', stdin=None, stdout=None, stderr=None)
 
         The optional argument 'completekey' is the readline name of a
@@ -151,9 +155,8 @@ def __init__(self, completekey='tab',
         if self.name is None:
             self.name = os.path.basename(sys.argv[0])
         if self.prompt is None:
-            self.prompt = self.name+"> "
+            self.prompt = self.name + "> "
         self._name_str = self._str(self.name)
-        self._prompt_str = self._str(self.prompt)
         if stdin is not None:
             self.stdin = stdin
         else:
@@ -183,9 +186,8 @@ def get_optparser(self):
         can return None to have no options at the top-level. Typically
         an instance of CmdlnOptionParser should be returned.
         """
-        version = (self.version is not None
-                    and "%s %s" % (self._name_str, self.version)
-                    or None)
+        version = (self.version is not None and
+                   "%s %s" % (self._name_str, self.version) or None)
         return CmdlnOptionParser(self, version=version)
 
     def postoptparse(self):
@@ -231,19 +233,19 @@ class MyCmd(cmdln.Cmdln):
             import sys
             argv = sys.argv
         else:
-            argv = argv[:] # don't modify caller's list
+            argv = argv[:]  # don't modify caller's list
 
         self.optparser = self.get_optparser()
-        if self.optparser: # i.e. optparser=None means don't process for opts
+        if self.optparser:  # i.e. optparser=None means don't process for opts
             try:
                 self.options, args = self.optparser.parse_args(argv[1:])
-            except CmdlnUserError, ex:
-                msg = "%s: %s\nTry '%s help' for info.\n"\
-                      % (self.name, ex, self.name)
+            except CmdlnUserError as ex:
+                msg = "%s: %s\nTry '%s help' for info.\n" % (self.name, ex,
+                                                             self.name)
                 self.stderr.write(self._str(msg))
                 self.stderr.flush()
                 return 1
-            except StopOptionProcessing, ex:
+            except StopOptionProcessing as ex:
                 return 0
         else:
             self.options, args = None, argv[1:]
@@ -320,7 +322,7 @@ def cmdloop(self, intro=None):
                 if sys.platform == "darwin":
                     readline.parse_and_bind("bind ^I rl_complete")
                 else:
-                    readline.parse_and_bind(self.completekey+": complete")
+                    readline.parse_and_bind(self.completekey + ": complete")
             except ImportError:
                 pass
         try:
@@ -328,7 +330,7 @@ def cmdloop(self, intro=None):
                 intro = self.intro
             if intro:
                 intro_str = self._str(intro)
-                self.stdout.write(intro_str+'\n')
+                self.stdout.write(intro_str + '\n')
             self.stop = False
             retval = None
             while not self.stop:
@@ -339,19 +341,19 @@ def cmdloop(self, intro=None):
                 else:
                     if self.use_rawinput:
                         try:
-                            line = raw_input(self._prompt_str)
+                            line = input(self._str(self._prompt_str))
                         except EOFError:
                             line = 'EOF'
                         except KeyboardInterrupt:
                             line = 'KeyboardInterrupt'
                     else:
-                        self.stdout.write(self._prompt_str)
+                        self.stdout.write(self._str(self._prompt_str))
                         self.stdout.flush()
                         line = self.stdin.readline()
                         if not len(line):
                             line = 'EOF'
                         else:
-                            line = line[:-1] # chop '\n'
+                            line = line[:-1]  # chop '\n'
                     argv = line2argv(line)
                 try:
                     argv = self.precmd(argv)
@@ -422,6 +424,8 @@ def onecmd(self, argv):
                     return self._dispatch_cmd(handler, argv)
                 except KeyboardInterrupt:
                     return self.onecmd(["KeyboardInterrupt"])
+                except Exception as ex:
+                    raise ex
         return self.default(argv)
 
     def _dispatch_cmd(self, handler, argv):
@@ -437,12 +441,12 @@ def default(self, argv):
 
         Returns a numeric command exit status.
         """
-        errmsg = self._str(self.unknowncmd % (argv[0],))
+        errmsg = self._str(self.unknowncmd % (argv[0], ))
         if self.cmdlooping:
-            self.stderr.write(errmsg+"\n")
+            self.stderr.write(errmsg + "\n")
         else:
-            self.stderr.write("%s: %s\nTry '%s help' for info.\n"
-                              % (self._name_str, errmsg, self._name_str))
+            self.stderr.write("%s: %s\nTry '%s help' for info.\n" %
+                              (self._name_str, errmsg, self._name_str))
         self.stderr.flush()
         return 1
 
@@ -456,7 +460,8 @@ def parseline(self, line):
         elif line[0] == '?':
             line = 'help ' + line[1:]
         i, n = 0, len(line)
-        while i < n and line[i] in self.identchars: i = i+1
+        while i < n and line[i] in self.identchars:
+            i = i + 1
         cmd, arg = line[:i], line[i:].strip()
         return cmd, arg, line
 
@@ -471,19 +476,19 @@ def helpdefault(self, cmd, known):
         Returns a return code.
         """
         if known:
-            msg = self._str(self.nohelp % (cmd,))
+            msg = self._str(self.nohelp % (cmd, ))
             if self.cmdlooping:
                 self.stderr.write(msg + '\n')
             else:
                 self.stderr.write("%s: %s\n" % (self.name, msg))
         else:
-            msg = self.unknowncmd % (cmd,)
+            msg = self.unknowncmd % (cmd, )
             if self.cmdlooping:
                 self.stderr.write(msg + '\n')
             else:
                 self.stderr.write("%s: %s\n"
-                                  "Try '%s help' for info.\n"
-                                  % (self.name, msg, self.name))
+                                  "Try '%s help' for info.\n" %
+                                  (self.name, msg, self.name))
         self.stderr.flush()
         return 1
 
@@ -493,13 +498,13 @@ def do_help(self, argv):
         Usage:
             ${name} help [COMMAND]
         """
-        if len(argv) > 1: # asking for help on a particular command
+        if len(argv) > 1:  # asking for help on a particular command
             doc = None
             cmdname = self._get_canonical_cmd_name(argv[1]) or argv[1]
             if not cmdname:
                 return self.helpdefault(argv[1], False)
             else:
-                helpfunc = getattr(self, "help_"+cmdname, None)
+                helpfunc = getattr(self, "help_" + cmdname, None)
                 if helpfunc:
                     doc = helpfunc()
                 else:
@@ -508,12 +513,12 @@ def do_help(self, argv):
                         doc = handler.__doc__
                     if doc is None:
                         return self.helpdefault(argv[1], handler != None)
-        else: # bare "help" command
+        else:  # bare "help" command
             doc = self.__class__.__doc__  # try class docstring
             if doc is None:
                 # Try to provide some reasonable useful default help.
                 if self.cmdlooping: prefix = ""
-                else:               prefix = self.name+' '
+                else: prefix = self.name + ' '
                 doc = """Usage:
                     %sCOMMAND [ARGS...]
                     %shelp [COMMAND]
@@ -524,12 +529,13 @@ def do_help(self, argv):
                 """ % (prefix, prefix)
             cmdname = None
 
-        if doc: # *do* have help content, massage and print that
+        if doc:  # *do* have help content, massage and print that
             doc = self._help_reindent(doc)
             doc = self._help_preprocess(doc, cmdname)
-            doc = doc.rstrip() + '\n' # trim down trailing space
+            doc = doc.rstrip() + '\n'  # trim down trailing space
             self.stdout.write(self._str(doc))
             self.stdout.flush()
+
     do_help.aliases = ["?"]
 
     def _help_reindent(self, help, indent=None):
@@ -557,7 +563,7 @@ def do_crush(self, ...):
             indent = self.helpindent
         lines = help.splitlines(0)
         _dedentlines(lines, skip_first_line=True)
-        lines = [(indent+line).rstrip() for line in lines]
+        lines = [(indent + line).rstrip() for line in lines]
         return '\n'.join(lines)
 
     def _help_preprocess(self, help, cmdname):
@@ -596,12 +602,12 @@ def _help_preprocess(self, help, cmdname):
         Returns the processed help.
         """
         preprocessors = {
-            "${name}":            self._help_preprocess_name,
-            "${option_list}":     self._help_preprocess_option_list,
-            "${command_list}":    self._help_preprocess_command_list,
-            "${help_list}":       self._help_preprocess_help_list,
-            "${cmd_name}":        self._help_preprocess_cmd_name,
-            "${cmd_usage}":       self._help_preprocess_cmd_usage,
+            "${name}": self._help_preprocess_name,
+            "${option_list}": self._help_preprocess_option_list,
+            "${command_list}": self._help_preprocess_command_list,
+            "${help_list}": self._help_preprocess_help_list,
+            "${cmd_name}": self._help_preprocess_cmd_name,
+            "${cmd_usage}": self._help_preprocess_cmd_usage,
             "${cmd_option_list}": self._help_preprocess_cmd_option_list,
         }
 
@@ -629,7 +635,7 @@ def _help_preprocess_option_list(self, help, cmdname=None):
         else:
             block = ""
 
-        help = help.replace(indent+marker+suffix, block, 1)
+        help = help.replace(indent + marker + suffix, block, 1)
         return help
 
     def _get_cmds_data(self):
@@ -642,14 +648,12 @@ def _get_cmds_data(self):
 
         # Get the list of (non-hidden) commands and their
         # documentation, if any.
-        cmdnames = {} # use a dict to strip duplicates
+        cmdnames = set()
         for attr in self.get_names():
             if attr.startswith("do_"):
-                cmdnames[attr[3:]] = True
-        cmdnames = cmdnames.keys()
-        cmdnames.sort()
+                cmdnames.add(attr[3:])
         linedata = []
-        for cmdname in cmdnames:
+        for cmdname in sorted(cmdnames):
             if aliases.get(cmdname):
                 a = aliases[cmdname]
                 a.sort()
@@ -658,7 +662,7 @@ def _get_cmds_data(self):
                 cmdstr = cmdname
             doc = None
             try:
-                helpfunc = getattr(self, 'help_'+cmdname)
+                helpfunc = getattr(self, 'help_' + cmdname)
             except AttributeError:
                 handler = self._get_cmd_handler(cmdname)
                 if handler:
@@ -674,7 +678,7 @@ def _get_cmds_data(self):
                 #log.debug("stripping %r from start of %s's help string",
                 #          to_strip, cmdname)
                 doc = doc[len(to_strip):].lstrip()
-            linedata.append( (cmdstr, doc) )
+            linedata.append((cmdstr, doc))
 
         return linedata
 
@@ -685,11 +689,11 @@ def _help_preprocess_command_list(self, help, cmdname=None):
 
         linedata = self._get_cmds_data()
         if linedata:
-            subindent = indent + ' '*4
-            lines = _format_linedata(linedata, subindent, indent_width+4)
+            subindent = indent + ' ' * 4
+            lines = _format_linedata(linedata, subindent, indent_width + 4)
             block = indent + "Commands:\n" \
                     + '\n'.join(lines) + "\n\n"
-            help = help.replace(indent+marker+suffix, block, 1)
+            help = help.replace(indent + marker + suffix, block, 1)
         return help
 
     def _gen_names_and_attrs(self):
@@ -726,15 +730,13 @@ def _help_preprocess_help_list(self, help, cmdname=None):
             linedata = [(n, a.__doc__ or "") for n, a in help_names.items()]
             linedata.sort()
 
-            subindent = indent + ' '*4
-            lines = _format_linedata(linedata, subindent, indent_width+4)
-            block = (indent
-                    + "Additional help topics (run `%s help TOPIC'):\n" % self.name
-                    + '\n'.join(lines)
-                    + "\n\n")
+            subindent = indent + ' ' * 4
+            lines = _format_linedata(linedata, subindent, indent_width + 4)
+            block = (indent + "Additional help topics (run `%s help TOPIC'):\n"
+                     % self.name + '\n'.join(lines) + "\n\n")
         else:
             block = ''
-        help = help.replace(indent+marker+suffix, block, 1)
+        help = help.replace(indent + marker + suffix, block, 1)
         return help
 
     def _help_preprocess_cmd_name(self, help, cmdname=None):
@@ -742,8 +744,8 @@ def _help_preprocess_cmd_name(self, help, cmdname=None):
         handler = self._get_cmd_handler(cmdname)
         if not handler:
             raise CmdlnError("cannot preprocess '%s' into help string: "
-                             "could not find command handler for %r"
-                             % (marker, cmdname))
+                             "could not find command handler for %r" %
+                             (marker, cmdname))
         s = cmdname
         if hasattr(handler, "aliases"):
             s += " (%s)" % (", ".join(handler.aliases))
@@ -758,33 +760,33 @@ def _help_preprocess_cmd_usage(self, help, cmdname=None):
         handler = self._get_cmd_handler(cmdname)
         if not handler:
             raise CmdlnError("cannot preprocess '%s' into help string: "
-                             "could not find command handler for %r"
-                             % (marker, cmdname))
+                             "could not find command handler for %r" %
+                             (marker, cmdname))
         indent, indent_width = _get_indent(marker, help)
         suffix = _get_trailing_whitespace(marker, help)
 
         # Extract the introspection bits we need.
-        func = handler.im_func
-        if func.func_defaults:
-            func_defaults = list(func.func_defaults)
+        func = handler.__func__
+        if func.__defaults__:
+            func_defaults = list(func.__defaults__)
         else:
             func_defaults = []
-        co_argcount = func.func_code.co_argcount
-        co_varnames = func.func_code.co_varnames
-        co_flags = func.func_code.co_flags
+        co_argcount = func.__code__.co_argcount
+        co_varnames = func.__code__.co_varnames
+        co_flags = func.__code__.co_flags
         CO_FLAGS_ARGS = 4
         CO_FLAGS_KWARGS = 8
 
         # Adjust argcount for possible *args and **kwargs arguments.
         argcount = co_argcount
-        if co_flags & CO_FLAGS_ARGS:   argcount += 1
+        if co_flags & CO_FLAGS_ARGS: argcount += 1
         if co_flags & CO_FLAGS_KWARGS: argcount += 1
 
         # Determine the usage string.
         usage = "%s %s" % (self.name, cmdname)
-        if argcount <= 2:   # handler ::= do_FOO(self, argv)
+        if argcount <= 2:  # handler ::= do_FOO(self, argv)
             usage += " [ARGS...]"
-        elif argcount >= 3: # handler ::= do_FOO(self, subcmd, opts, ...)
+        elif argcount >= 3:  # handler ::= do_FOO(self, subcmd, opts, ...)
             argnames = list(co_varnames[3:argcount])
             tail = ""
             if co_flags & CO_FLAGS_KWARGS:
@@ -794,9 +796,8 @@ def _help_preprocess_cmd_usage(self, help, cmdname=None):
                 # keyword arguments from the command line. Could
                 # *perhaps* consider: arg=value arg2=value2 ...
                 warnings.warn("argument '**%s' on '%s.%s' command "
-                              "handler will never get values"
-                              % (name, self.__class__.__name__,
-                                 func.func_name))
+                              "handler will never get values" %
+                              (name, self.__class__.__name__, func.func_name))
             if co_flags & CO_FLAGS_ARGS:
                 name = argnames.pop(-1)
                 tail = "[%s...]" % name.upper()
@@ -810,12 +811,11 @@ def _help_preprocess_cmd_usage(self, help, cmdname=None):
             usage += ' ' + tail
 
         block_lines = [
-            self.helpindent + "Usage:",
-            self.helpindent + ' '*4 + usage
+            self.helpindent + "Usage:", self.helpindent + ' ' * 4 + usage
         ]
         block = '\n'.join(block_lines) + '\n\n'
 
-        help = help.replace(indent+marker+suffix, block, 1)
+        help = help.replace(indent + marker + suffix, block, 1)
         return help
 
     #TODO: this only makes sense as part of the Cmdln class.
@@ -826,8 +826,8 @@ def _help_preprocess_cmd_option_list(self, help, cmdname=None):
         handler = self._get_cmd_handler(cmdname)
         if not handler:
             raise CmdlnError("cannot preprocess '%s' into help string: "
-                             "could not find command handler for %r"
-                             % (marker, cmdname))
+                             "could not find command handler for %r" %
+                             (marker, cmdname))
         indent, indent_width = _get_indent(marker, help)
         suffix = _get_trailing_whitespace(marker, help)
         if hasattr(handler, "optparser"):
@@ -841,7 +841,7 @@ def _help_preprocess_cmd_option_list(self, help, cmdname=None):
         else:
             block = ""
 
-        help = help.replace(indent+marker+suffix, block, 1)
+        help = help.replace(indent + marker + suffix, block, 1)
         return help
 
     def _get_canonical_cmd_name(self, token):
@@ -856,23 +856,23 @@ def _get_canonical_map(self):
         if not hasattr(self, cacheattr):
             # Get the list of commands and their aliases, if any.
             token2canonical = {}
-            cmd2funcname = {} # use a dict to strip duplicates
+            cmd2funcname = {}  # use a dict to strip duplicates
             for attr in self.get_names():
-                if attr.startswith("do_"):    cmdname = attr[3:]
+                if attr.startswith("do_"): cmdname = attr[3:]
                 elif attr.startswith("_do_"): cmdname = attr[4:]
                 else:
                     continue
                 cmd2funcname[cmdname] = attr
                 token2canonical[cmdname] = cmdname
-            for cmdname, funcname in cmd2funcname.items(): # add aliases
+            for cmdname, funcname in cmd2funcname.items():  # add aliases
                 func = getattr(self, funcname)
                 aliases = getattr(func, "aliases", [])
                 for alias in aliases:
                     if alias in cmd2funcname:
                         import warnings
                         warnings.warn("'%s' alias for '%s' command conflicts "
-                                      "with '%s' handler"
-                                      % (alias, cmdname, cmd2funcname[alias]))
+                                      "with '%s' handler" %
+                                      (alias, cmdname, cmd2funcname[alias]))
                         continue
                     token2canonical[alias] = cmdname
             setattr(self, cacheattr, token2canonical)
@@ -913,12 +913,12 @@ def emptyline(self):
         else:
             return self.do_help(["help"])
 
-
 #---- optparse.py extension to fix (IMO) some deficiencies
 #
 # See the class _OptionParserEx docstring for details.
 #
 
+
 class StopOptionProcessing(Exception):
     """Indicate that option *and argument* processing should stop
     cleanly. This is not an error condition. It is similar in spirit to
@@ -939,6 +939,7 @@ class StopOptionProcessing(Exception):
             sys.exit(0)
     """
 
+
 class _OptionParserEx(optparse.OptionParser):
     """An optparse.OptionParser that uses exceptions instead of sys.exit.
 
@@ -961,6 +962,7 @@ class _OptionParserEx(optparse.OptionParser):
       These are good practices, just not valid defaults if they can
       get in the way.
     """
+
     def error(self, msg):
         raise optparse.OptParseError(msg)
 
@@ -971,10 +973,9 @@ def exit(self, status=0, msg=None):
             #TODO: don't lose status info here
             raise optparse.OptParseError(msg)
 
-
-
 #---- optparse.py-based option processing support
 
+
 class CmdlnOptionParser(_OptionParserEx):
     """An optparse.OptionParser class more appropriate for top-level
     Cmdln options. For parsing of sub-command options, see
@@ -990,6 +991,7 @@ class CmdlnOptionParser(_OptionParserEx):
       make error handling clearer.
     - Also see the changes in _OptionParserEx.
     """
+
     def __init__(self, cmdln, **kwargs):
         self.cmdln = cmdln
         kwargs["prog"] = self.cmdln.name
@@ -1028,6 +1030,7 @@ class MyShell(cmdln.Cmdln):
             def do_remove(self, subcmd, opts, *args):
                 #...
     """
+
     #XXX Is there a possible optimization for many options to not have a
     #    large stack depth here?
     def decorate(f):
@@ -1035,6 +1038,7 @@ def decorate(f):
             f.optparser = SubCmdOptionParser()
         f.optparser.add_option(*args, **kwargs)
         return f
+
     return decorate
 
 
@@ -1067,6 +1071,7 @@ def do_status(self, subcmd, opts, *paths):
     integration.  See this class' _dispatch_cmd() docstring and general
     cmdln document for more information.
     """
+
     def _dispatch_cmd(self, handler, argv):
         """Introspect sub-command handler signature to determine how to
         dispatch the command. The raw handler provided by the base
@@ -1104,14 +1109,14 @@ def do_bar2(self, subcmd, opts, bar_one, bar_two):
         and an appropriate error message will be raised/printed if the
         command is called with a different number of args.
         """
-        co_argcount = handler.im_func.func_code.co_argcount
-        if co_argcount == 2:   # handler ::= do_foo(self, argv)
+        co_argcount = handler.__func__.__code__.co_argcount
+        if co_argcount == 2:  # handler ::= do_foo(self, argv)
             return handler(argv)
-        elif co_argcount >= 3: # handler ::= do_foo(self, subcmd, opts, ...)
+        elif co_argcount >= 3:  # handler ::= do_foo(self, subcmd, opts, ...)
             try:
                 optparser = handler.optparser
             except AttributeError:
-                optparser = handler.im_func.optparser = SubCmdOptionParser()
+                optparser = handler.__func__.optparser = SubCmdOptionParser()
             assert isinstance(optparser, SubCmdOptionParser)
             optparser.set_cmdln_info(self, argv[0])
             try:
@@ -1119,19 +1124,16 @@ def do_bar2(self, subcmd, opts, bar_one, bar_two):
             except StopOptionProcessing:
                 #TODO: this doesn't really fly for a replacement of
                 #      optparse.py behaviour, does it?
-                return 0 # Normal command termination
+                return 0  # Normal command termination
 
             try:
                 return handler(argv[0], opts, *args)
-            except TypeError, ex:
-                # Some TypeError's are user errors:
-                #   do_foo() takes at least 4 arguments (3 given)
-                #   do_foo() takes at most 5 arguments (6 given)
-                #   do_foo() takes exactly 5 arguments (6 given)
-                # Raise CmdlnUserError for these with a suitably
+            except TypeError as ex:
+                # Some TypeError's are user errors because of incorrect number
+                # of arguments. Raise CmdlnUserError for these with a suitably
                 # massaged error message.
                 import sys
-                tb = sys.exc_info()[2] # the traceback object
+                tb = sys.exc_info()[2]  # the traceback object
                 if tb.tb_next is not None:
                     # If the traceback is more than one level deep, then the
                     # TypeError do *not* happen on the "handler(...)" call
@@ -1139,15 +1141,9 @@ def do_bar2(self, subcmd, opts, bar_one, bar_two):
                     # here: it would falsely mask deeper code errors.
                     raise
                 msg = ex.args[0]
-                match = _INCORRECT_NUM_ARGS_RE.search(msg)
-                if match:
-                    msg = list(match.groups())
-                    msg[1] = int(msg[1]) - 3
-                    if msg[1] == 1:
-                        msg[2] = msg[2].replace("arguments", "argument")
-                    msg[3] = int(msg[3]) - 3
-                    msg = ''.join(map(str, msg))
-                    raise CmdlnUserError(msg)
+                userErr = self._userErrFromNumArgsErrmsg(msg)
+                if userErr:
+                    raise userErr
                 else:
                     raise
         else:
@@ -1155,10 +1151,37 @@ def do_bar2(self, subcmd, opts, bar_one, bar_two):
                              "take 2 for 'argv' signature or 3+ for 'opts' "
                              "signature" % (handler.__name__, co_argcount))
 
-
+    def _userErrFromNumArgsErrmsg(self, msg):
+        if sys.version_info[0] < 3:
+            # Examples (see Python/getargs.c):
+            #   do_foo() takes at least 4 arguments (3 given)
+            #   do_foo() takes at most 5 arguments (6 given)
+            #   do_foo() takes exactly 5 arguments (6 given)
+            pattern = re.compile(
+                r"(takes [\w ]+ )(\d+)( arguments? \()(\d+)( given\))")
+            match = pattern.search(msg)
+            if match:
+                msg = list(match.groups())
+                msg[1] = int(msg[1]) - 3
+                if msg[1] == 1:
+                    msg[2] = msg[2].replace("arguments", "argument")
+                msg[3] = int(msg[3]) - 3
+                msg = ''.join(map(str, msg))
+                return CmdlnUserError(msg)
+        else:
+            # Examples:
+            #   do_foo() missing 1 required positional argument: 'bar'
+            patterns = [
+                re.compile(r"missing \d+ required positional argument"),
+            ]
+            for pat in patterns:
+                match = pat.search(msg)
+                if match:
+                    return CmdlnUserError("incorrect number of arguments")
 
 #---- support for generating `man` page output from a Cmdln class
 
+
 def man_sections_from_cmdln(inst, summary=None, description=None, author=None):
     """Return man page sections appropriate for the given Cmdln instance.
     Join these sections for man page content.
@@ -1183,7 +1206,7 @@ def man_sections_from_cmdln(inst, summary=None, description=None, author=None):
     """
     if not inst.__class__.name:
         raise ValueError("cannot generate man page content: `name` is not "
-            "set on class %r" % inst.__class__)
+                         "set on class %r" % inst.__class__)
     data = {
         "name": inst.name,
         "ucname": inst.name.upper(),
@@ -1194,7 +1217,8 @@ def man_sections_from_cmdln(inst, summary=None, description=None, author=None):
     }
 
     sections = []
-    sections.append('.\\" Automatically generated by cmdln %(cmdln_version)s\n'
+    sections.append(
+        '.\\" Automatically generated by cmdln %(cmdln_version)s\n'
         '.TH %(ucname)s "1" "%(date)s" "%(name)s%(version_str)s" "User Commands"\n'
         % data)
     sections.append(".SH NAME\n%(name)s%(summary_str)s\n" % data)
@@ -1248,10 +1272,9 @@ def man_sections_from_cmdln(inst, summary=None, description=None, author=None):
 
     return sections
 
-
-
 #---- internal support functions
 
+
 def _format_linedata(linedata, indent, indent_width):
     """Format specific linedata into a pleasant layout.
 
@@ -1268,7 +1291,7 @@ def _format_linedata(linedata, indent, indent_width):
     SPACING = 2
     NAME_WIDTH_LOWER_BOUND = 13
     NAME_WIDTH_UPPER_BOUND = 30
-    NAME_WIDTH = max([len(s) for s,d in linedata])
+    NAME_WIDTH = max([len(s) for s, d in linedata])
     if NAME_WIDTH < NAME_WIDTH_LOWER_BOUND:
         NAME_WIDTH = NAME_WIDTH_LOWER_BOUND
     elif NAME_WIDTH > NAME_WIDTH_UPPER_BOUND:
@@ -1286,6 +1309,7 @@ def _format_linedata(linedata, indent, indent_width):
         lines.append(line.rstrip())
     return lines
 
+
 def _summarize_doc(doc, length=60):
     r"""Parse out a short one line summary from the given doclines.
 
@@ -1320,7 +1344,7 @@ def _summarize_doc(doc, length=60):
 
     summary = ' '.join(summlines)
     if len(summary) > length:
-        summary = summary[:length-3] + "..."
+        summary = summary[:length - 3] + "..."
     return summary
 
 
@@ -1385,7 +1409,7 @@ def line2argv(line):
     ...     line2argv(r'\\foo\\bar') == ['\\foo\\bar']
     ...     try:
     ...         line2argv('"foo')
-    ...     except ValueError, ex:
+    ...     except ValueError as ex:
     ...         "not terminated" in str(ex)
     True
     True
@@ -1402,12 +1426,12 @@ def line2argv(line):
         if i >= len(line): break
         ch = line[i]
 
-        if ch == "\\" and i+1 < len(line):
+        if ch == "\\" and i + 1 < len(line):
             # escaped char always added to arg, regardless of state
             if arg is None: arg = ""
-            if (sys.platform == "win32"
-                or state in ("double-quoted", "single-quoted")
-               ) and line[i+1] not in tuple('"\''):
+            if (sys.platform == "win32" or
+                    state in ("double-quoted", "single-quoted")
+                ) and line[i + 1] not in tuple('"\''):
                 arg += ch
             i += 1
             arg += line[i]
@@ -1459,9 +1483,9 @@ def argv2line(argv):
     'foo "bar baz"'
     >>> argv2line(['foo"bar'])
     'foo"bar'
-    >>> print argv2line(['foo" bar'])
+    >>> print(argv2line(['foo" bar']))
     'foo" bar'
-    >>> print argv2line(["foo' bar"])
+    >>> print(argv2line(["foo' bar"]))
     "foo' bar"
     >>> argv2line(["foo'bar"])
     "foo'bar"
@@ -1469,12 +1493,12 @@ def argv2line(argv):
     escapedArgs = []
     for arg in argv:
         if ' ' in arg and '"' not in arg:
-            arg = '"'+arg+'"'
+            arg = '"' + arg + '"'
         elif ' ' in arg and "'" not in arg:
-            arg = "'"+arg+"'"
+            arg = "'" + arg + "'"
         elif ' ' in arg:
             arg = arg.replace('"', r'\"')
-            arg = '"'+arg+'"'
+            arg = '"' + arg + '"'
         escapedArgs.append(arg)
     return ' '.join(escapedArgs)
 
@@ -1494,8 +1518,8 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False):
     """
     DEBUG = False
     if DEBUG:
-        print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
-              % (tabsize, skip_first_line)
+        print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\
+              % (tabsize, skip_first_line))
     indents = []
     margin = None
     for i, line in enumerate(lines):
@@ -1507,17 +1531,17 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False):
             elif ch == '\t':
                 indent += tabsize - (indent % tabsize)
             elif ch in '\r\n':
-                continue # skip all-whitespace lines
+                continue  # skip all-whitespace lines
             else:
                 break
         else:
-            continue # skip all-whitespace lines
-        if DEBUG: print "dedent: indent=%d: %r" % (indent, line)
+            continue  # skip all-whitespace lines
+        if DEBUG: print("dedent: indent=%d: %r" % (indent, line))
         if margin is None:
             margin = indent
         else:
             margin = min(margin, indent)
-    if DEBUG: print "dedent: margin=%r" % margin
+    if DEBUG: print("dedent: margin=%r" % margin)
 
     if margin is not None and margin > 0:
         for i, line in enumerate(lines):
@@ -1529,24 +1553,26 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False):
                 elif ch == '\t':
                     removed += tabsize - (removed % tabsize)
                 elif ch in '\r\n':
-                    if DEBUG: print "dedent: %r: EOL -> strip up to EOL" % line
+                    if DEBUG:
+                        print("dedent: %r: EOL -> strip up to EOL" % line)
                     lines[i] = lines[i][j:]
                     break
                 else:
                     raise ValueError("unexpected non-whitespace char %r in "
-                                     "line %r while removing %d-space margin"
-                                     % (ch, line, margin))
+                                     "line %r while removing %d-space margin" %
+                                     (ch, line, margin))
                 if DEBUG:
-                    print "dedent: %r: %r -> removed %d/%d"\
-                          % (line, ch, removed, margin)
+                    print("dedent: %r: %r -> removed %d/%d"\
+                          % (line, ch, removed, margin))
                 if removed == margin:
-                    lines[i] = lines[i][j+1:]
+                    lines[i] = lines[i][j + 1:]
                     break
                 elif removed > margin:
-                    lines[i] = ' '*(removed-margin) + lines[i][j+1:]
+                    lines[i] = ' ' * (removed - margin) + lines[i][j + 1:]
                     break
     return lines
 
+
 def _dedent(text, tabsize=8, skip_first_line=False):
     """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text
 
@@ -1562,6 +1588,7 @@ def _dedent(text, tabsize=8, skip_first_line=False):
     _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)
     return ''.join(lines)
 
+
 def _get_indent(marker, s, tab_width=8):
     """_get_indent(marker, s, tab_width=8) ->
         (<indentation-of-'marker'>, <indentation-width>)"""
@@ -1570,7 +1597,7 @@ def _get_indent(marker, s, tab_width=8):
     start = s.index(marker)
     i = start
     while i > 0:
-        if s[i-1] not in INDENT_CHARS:
+        if s[i - 1] not in INDENT_CHARS:
             break
         i -= 1
     indent = s[i:start]
@@ -1582,6 +1609,7 @@ def _get_indent(marker, s, tab_width=8):
             indent_width += tab_width - (indent_width % tab_width)
     return indent, indent_width
 
+
 def _get_trailing_whitespace(marker, s):
     """Return the whitespace content trailing the given 'marker' in string 's',
     up to and including a newline.
@@ -1594,16 +1622,14 @@ def _get_trailing_whitespace(marker, s):
             suffix += s[i]
         elif s[i] in '\r\n':
             suffix += s[i]
-            if s[i] == '\r' and i+1 < len(s) and s[i+1] == '\n':
-                suffix += s[i+1]
+            if s[i] == '\r' and i + 1 < len(s) and s[i + 1] == '\n':
+                suffix += s[i + 1]
             break
         else:
             break
         i += 1
     return suffix
 
-
-
 #---- bash completion support
 # Note: This is still experimental. I expect to change this
 # significantly.
@@ -1619,8 +1645,9 @@ def _get_trailing_whitespace(marker, s):
 #      optional if there is only one in the module (common case).
 
 if __name__ == "__main__" and len(sys.argv) == 6:
+
     def _log(s):
-        return # no-op, comment out for debugging
+        return  # no-op, comment out for debugging
         from os.path import expanduser
         fout = open(expanduser("~/tmp/bashcpln.log"), 'a')
         fout.write(str(s) + '\n')
@@ -1639,11 +1666,11 @@ def _module_from_path(path):
         finally:
             sys.path.remove(dir)
 
-    def _get_bash_cplns(script_path, class_name, cmd_name,
-                        token, preceding_token):
+    def _get_bash_cplns(script_path, class_name, cmd_name, token,
+                        preceding_token):
         _log('--')
-        _log('get_cplns(%r, %r, %r, %r, %r)'
-             % (script_path, class_name, cmd_name, token, preceding_token))
+        _log('get_cplns(%r, %r, %r, %r, %r)' %
+             (script_path, class_name, cmd_name, token, preceding_token))
         comp_line = os.environ["COMP_LINE"]
         comp_point = int(os.environ["COMP_POINT"])
         _log("COMP_LINE: %r" % comp_line)
@@ -1651,7 +1678,7 @@ def _get_bash_cplns(script_path, class_name, cmd_name,
 
         try:
             script = _module_from_path(script_path)
-        except ImportError, ex:
+        except ImportError as ex:
             _log("error importing `%s': %s" % (script_path, ex))
             return []
         shell = getattr(script, class_name)()
@@ -1700,4 +1727,4 @@ def _get_bash_cplns(script_path, class_name, cmd_name,
         return []
 
     for cpln in _get_bash_cplns(*sys.argv[1:]):
-        print cpln
+        print(cpln)
diff --git a/manta/errors.py b/manta/errors.py
index 26cfb2a..9406cf9 100644
--- a/manta/errors.py
+++ b/manta/errors.py
@@ -1,38 +1,39 @@
 # Copyright 2012 Joyent, Inc.  All rights reserved.
-
 """python-manta errors"""
 
+from __future__ import absolute_import
+
 __all__ = ["MantaError", "MantaResourceNotFoundError", "MantaAPIError"]
 
 import logging
 import json
 
-
-
 #---- globals
 
 log = logging.getLogger('manta.errors')
 
-
-
 #---- exports
 
+
 class MantaError(Exception):
     pass
 
+
 class MantaResourceNotFoundError(Exception):
     pass
 
+
 class MantaAPIError(MantaError):
     """An errors from the Manta API.
 
     @param res {httplib2 Response}
     @param content {str} The raw response content.
     """
+
     def __init__(self, res, content):
         self.res = res
         if res['content-type'] == 'application/json':
-            self.body = json.loads(content)
+            self.body = json.loads(content.decode('utf-8'))
             self.code = self.body["code"]
             message = "(%(code)s) %(message)s" % self.body
         else:
diff --git a/manta/version.py b/manta/version.py
index 396266f..3d7447f 100644
--- a/manta/version.py
+++ b/manta/version.py
@@ -1,7 +1,9 @@
 # Copyright (c) 2018, Joyent, Inc.
 
+from __future__ import print_function
+
 __version_info__ = (2, 7, 0)
 __version__ = ".".join(map(str, __version_info__))
 
 if __name__ == "__main__":
-    print __version__
+    print (__version__)
diff --git a/setup.py b/setup.py
index 9b02964..78f6ff6 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-
 """python-manta Python package setup script"""
 
 import os
@@ -13,24 +12,26 @@
 except ImportError:
     from distutils.core import setup
 
-
 TOP = os.path.dirname(os.path.abspath(__file__))
 
+
 def get_version():
     """Get the python-manta version without having to import the manta package,
     which requires deps to already be installed.
     """
     _globals = {}
     _locals = {}
-    execfile(TOP + "/manta/version.py", _globals, _locals)
+    exec(
+        compile(
+            open(TOP + "/manta/version.py").read(), TOP + "/manta/version.py",
+            'exec'), _globals, _locals)
     return _locals["__version__"]
 
 
-setup(
-    name="manta",
-    version=get_version(),
-    description="A Python SDK for Joyent's Manta service",
-    long_description="""`Manta <http://www.joyent.com/products/manta>`_
+setup(name="manta",
+      version=get_version(),
+      description="A Python SDK for Joyent's Manta service",
+      long_description="""`Manta <http://www.joyent.com/products/manta>`_
 is Joyent's object storage service with integrate compute.  This module
 provides a Python 'manta' package and a 'mantash' (MANTA SHell) CLI and
 shell. The project is `hosted on GitHub
@@ -67,5 +68,4 @@ def get_version():
     scripts=[
         (sys.platform == "win32" and "bin\\mantash" or "bin/mantash")
     ],
-    zip_safe=False,
-)
+    zip_safe=False, )
diff --git a/tools/which.py b/tools/which.py
new file mode 100755
index 0000000..83e431b
--- /dev/null
+++ b/tools/which.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2007 ActiveState Software Inc.
+# See LICENSE.txt for license details.
+# Author:
+#   Trent Mick (TrentM@ActiveState.com)
+# Home:
+#   http://trentm.com/projects/which/
+
+r"""Find the full path to commands.
+
+which(command, path=None, verbose=0, exts=None)
+    Return the full path to the first match of the given command on the
+    path.
+
+whichall(command, path=None, verbose=0, exts=None)
+    Return a list of full paths to all matches of the given command on
+    the path.
+
+whichgen(command, path=None, verbose=0, exts=None)
+    Return a generator which will yield full paths to all matches of the
+    given command on the path.
+    
+By default the PATH environment variable is searched (as well as, on
+Windows, the AppPaths key in the registry), but a specific 'path' list
+to search may be specified as well.  On Windows, the PATHEXT environment
+variable is applied as appropriate.
+
+If "verbose" is true then a tuple of the form
+    (<fullpath>, <matched-where-description>)
+is returned for each match. The latter element is a textual description
+of where the match was found. For example:
+    from PATH element 0
+    from HKLM\SOFTWARE\...\perl.exe
+"""
+
+from __future__ import print_function
+
+_cmdlnUsage = """
+    Show the full path of commands.
+
+    Usage:
+        which [<options>...] [<command-name>...]
+
+    Options:
+        -h, --help      Print this help and exit.
+        -V, --version   Print the version info and exit.
+
+        -a, --all       Print *all* matching paths.
+        -v, --verbose   Print out how matches were located and
+                        show near misses on stderr.
+        -q, --quiet     Just print out matches. I.e., do not print out
+                        near misses.
+
+        -p <altpath>, --path=<altpath>
+                        An alternative path (list of directories) may
+                        be specified for searching.
+        -e <exts>, --exts=<exts>
+                        Specify a list of extensions to consider instead
+                        of the usual list (';'-separate list, Windows
+                        only).
+
+    Show the full path to the program that would be run for each given
+    command name, if any. Which, like GNU's which, returns the number of
+    failed arguments, or -1 when no <command-name> was given.
+
+    Near misses include duplicates, non-regular files and (on Un*x)
+    files without executable access.
+"""
+
+__revision__ = "$Id$"
+__version_info__ = (1, 1, 3)
+__version__ = '.'.join(map(str, __version_info__))
+__all__ = ["which", "whichall", "whichgen", "WhichError"]
+
+import os
+import sys
+import getopt
+import stat
+
+
+#---- exceptions
+
+class WhichError(Exception):
+    pass
+
+
+
+#---- internal support stuff
+
+def _getRegisteredExecutable(exeName):
+    """Windows allow application paths to be registered in the registry."""
+    registered = None
+    if sys.platform.startswith('win'):
+        if os.path.splitext(exeName)[1].lower() != '.exe':
+            exeName += '.exe'
+        import _winreg
+        try:
+            key = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" +\
+                  exeName
+            value = _winreg.QueryValue(_winreg.HKEY_LOCAL_MACHINE, key)
+            registered = (value, "from HKLM\\"+key)
+        except _winreg.error:
+            pass
+        if registered and not os.path.exists(registered[0]):
+            registered = None
+    return registered
+
+def _samefile(fname1, fname2):
+    if sys.platform.startswith('win'):
+        return ( os.path.normpath(os.path.normcase(fname1)) ==\
+            os.path.normpath(os.path.normcase(fname2)) )
+    else:
+        return os.path.samefile(fname1, fname2)
+
+def _cull(potential, matches, verbose=0):
+    """Cull inappropriate matches. Possible reasons:
+        - a duplicate of a previous match
+        - not a disk file
+        - not executable (non-Windows)
+    If 'potential' is approved it is returned and added to 'matches'.
+    Otherwise, None is returned.
+    """
+    for match in matches:  # don't yield duplicates
+        if _samefile(potential[0], match[0]):
+            if verbose:
+                sys.stderr.write("duplicate: %s (%s)\n" % potential)
+            return None
+    else:
+        if not stat.S_ISREG(os.stat(potential[0]).st_mode):
+            if verbose:
+                sys.stderr.write("not a regular file: %s (%s)\n" % potential)
+        elif sys.platform != "win32" \
+             and not os.access(potential[0], os.X_OK):
+            if verbose:
+                sys.stderr.write("no executable access: %s (%s)\n"\
+                                 % potential)
+        else:
+            matches.append(potential)
+            return potential
+
+        
+#---- module API
+
+def whichgen(command, path=None, verbose=0, exts=None):
+    """Return a generator of full paths to the given command.
+    
+    "command" is a the name of the executable to search for.
+    "path" is an optional alternate path list to search. The default it
+        to use the PATH environment variable.
+    "verbose", if true, will cause a 2-tuple to be returned for each
+        match. The second element is a textual description of where the
+        match was found.
+    "exts" optionally allows one to specify a list of extensions to use
+        instead of the standard list for this system. This can
+        effectively be used as an optimization to, for example, avoid
+        stat's of "foo.vbs" when searching for "foo" and you know it is
+        not a VisualBasic script but ".vbs" is on PATHEXT. This option
+        is only supported on Windows.
+
+    This method returns a generator which yields either full paths to
+    the given command or, if verbose, tuples of the form (<path to
+    command>, <where path found>).
+    """
+    matches = []
+    if path is None:
+        usingGivenPath = 0
+        path = os.environ.get("PATH", "").split(os.pathsep)
+        if sys.platform.startswith("win"):
+            path.insert(0, os.curdir)  # implied by Windows shell
+    else:
+        usingGivenPath = 1
+
+    # Windows has the concept of a list of extensions (PATHEXT env var).
+    if sys.platform.startswith("win"):
+        if exts is None:
+            exts = os.environ.get("PATHEXT", "").split(os.pathsep)
+            # If '.exe' is not in exts then obviously this is Win9x and
+            # or a bogus PATHEXT, then use a reasonable default.
+            for ext in exts:
+                if ext.lower() == ".exe":
+                    break
+            else:
+                exts = ['.COM', '.EXE', '.BAT']
+        elif not isinstance(exts, list):
+            raise TypeError("'exts' argument must be a list or None")
+    else:
+        if exts is not None:
+            raise WhichError("'exts' argument is not supported on "\
+                             "platform '%s'" % sys.platform)
+        exts = []
+
+    # File name cannot have path separators because PATH lookup does not
+    # work that way.
+    if os.sep in command or os.altsep and os.altsep in command:
+        if os.path.exists(command):
+            match = _cull((command, "explicit path given"), matches, verbose)
+            if verbose:
+                yield match
+            else:
+                yield match[0]
+    else:
+        for i in range(len(path)):
+            dirName = path[i]
+            # On windows the dirName *could* be quoted, drop the quotes
+            if sys.platform.startswith("win") and len(dirName) >= 2\
+               and dirName[0] == '"' and dirName[-1] == '"':
+                dirName = dirName[1:-1]
+            for ext in ['']+exts:
+                absName = os.path.abspath(
+                    os.path.normpath(os.path.join(dirName, command+ext)))
+                if os.path.isfile(absName):
+                    if usingGivenPath:
+                        fromWhere = "from given path element %d" % i
+                    elif not sys.platform.startswith("win"):
+                        fromWhere = "from PATH element %d" % i
+                    elif i == 0:
+                        fromWhere = "from current directory"
+                    else:
+                        fromWhere = "from PATH element %d" % (i-1)
+                    match = _cull((absName, fromWhere), matches, verbose)
+                    if match:
+                        if verbose:
+                            yield match
+                        else:
+                            yield match[0]
+        match = _getRegisteredExecutable(command)
+        if match is not None:
+            match = _cull(match, matches, verbose)
+            if match:
+                if verbose:
+                    yield match
+                else:
+                    yield match[0]
+
+
+def which(command, path=None, verbose=0, exts=None):
+    """Return the full path to the first match of the given command on
+    the path.
+    
+    "command" is a the name of the executable to search for.
+    "path" is an optional alternate path list to search. The default it
+        to use the PATH environment variable.
+    "verbose", if true, will cause a 2-tuple to be returned. The second
+        element is a textual description of where the match was found.
+    "exts" optionally allows one to specify a list of extensions to use
+        instead of the standard list for this system. This can
+        effectively be used as an optimization to, for example, avoid
+        stat's of "foo.vbs" when searching for "foo" and you know it is
+        not a VisualBasic script but ".vbs" is on PATHEXT. This option
+        is only supported on Windows.
+
+    If no match is found for the command, a WhichError is raised.
+    """
+    try:
+        match = whichgen(command, path, verbose, exts).next()
+    except StopIteration:
+        raise WhichError("Could not find '%s' on the path." % command)
+    return match
+
+
+def whichall(command, path=None, verbose=0, exts=None):
+    """Return a list of full paths to all matches of the given command
+    on the path.  
+
+    "command" is a the name of the executable to search for.
+    "path" is an optional alternate path list to search. The default it
+        to use the PATH environment variable.
+    "verbose", if true, will cause a 2-tuple to be returned for each
+        match. The second element is a textual description of where the
+        match was found.
+    "exts" optionally allows one to specify a list of extensions to use
+        instead of the standard list for this system. This can
+        effectively be used as an optimization to, for example, avoid
+        stat's of "foo.vbs" when searching for "foo" and you know it is
+        not a VisualBasic script but ".vbs" is on PATHEXT. This option
+        is only supported on Windows.
+    """
+    return list( whichgen(command, path, verbose, exts) )
+
+
+
+#---- mainline
+
+def main(argv):
+    all = 0
+    verbose = 0
+    altpath = None
+    exts = None
+    try:
+        optlist, args = getopt.getopt(argv[1:], 'haVvqp:e:',
+            ['help', 'all', 'version', 'verbose', 'quiet', 'path=', 'exts='])
+    except getopt.GetoptError as msg:
+        sys.stderr.write("which: error: %s. Your invocation was: %s\n"\
+                         % (msg, argv))
+        sys.stderr.write("Try 'which --help'.\n")
+        return 1
+    for opt, optarg in optlist:
+        if opt in ('-h', '--help'):
+            print(_cmdlnUsage)
+            return 0
+        elif opt in ('-V', '--version'):
+            print("which %s" % __version__)
+            return 0
+        elif opt in ('-a', '--all'):
+            all = 1
+        elif opt in ('-v', '--verbose'):
+            verbose = 1
+        elif opt in ('-q', '--quiet'):
+            verbose = 0
+        elif opt in ('-p', '--path'):
+            if optarg:
+                altpath = optarg.split(os.pathsep)
+            else:
+                altpath = []
+        elif opt in ('-e', '--exts'):
+            if optarg:
+                exts = optarg.split(os.pathsep)
+            else:
+                exts = []
+
+    if len(args) == 0:
+        return -1
+
+    failures = 0
+    for arg in args:
+        #print "debug: search for %r" % arg
+        nmatches = 0
+        for match in whichgen(arg, path=altpath, verbose=verbose, exts=exts):
+            if verbose:
+                print("%s (%s)" % match)
+            else:
+                print(match)
+            nmatches += 1
+            if not all:
+                break
+        if not nmatches:
+            failures += 1
+    return failures
+
+
+if __name__ == "__main__":
+    sys.exit( main(sys.argv) )
+
+


Test patch:
diff --git a/test/common.py b/test/common.py
index 79f06a7..2a21260 100644
--- a/test/common.py
+++ b/test/common.py
@@ -1,8 +1,9 @@
 #!/usr/bin/env python
 # Copyright (c) 2012 Joyent, Inc.  All rights reserved.
-
 """Shared code for test case files."""
 
+from __future__ import absolute_import
+
 __all__ = ["stor", "MantaTestCase"]
 
 import sys
@@ -14,10 +15,9 @@
 
 import manta
 
-
-
 #---- exports
 
+
 def stor(*subpaths):
     MANTA_USER = os.environ['MANTA_USER']
     if not subpaths:
@@ -27,6 +27,7 @@ def stor(*subpaths):
         subpath = subpath[1:]
     return "/%s/stor/%s" % (MANTA_USER, subpath)
 
+
 class MantaTestCase(unittest.TestCase):
     def __init__(self, *args):
         self.account = os.environ["MANTA_USER"]
@@ -35,13 +36,15 @@ def __init__(self, *args):
         unittest.TestCase.__init__(self, *args)
 
     _client = None
+
     def get_client(self):
         MANTA_URL = os.environ['MANTA_URL']
         MANTA_KEY_ID = os.environ['MANTA_KEY_ID']
         MANTA_TLS_INSECURE = bool(os.environ.get('MANTA_TLS_INSECURE', False))
         if not self._client:
             signer = manta.CLISigner(key_id=MANTA_KEY_ID)
-            self._client = manta.MantaClient(url=MANTA_URL,
+            self._client = manta.MantaClient(
+                url=MANTA_URL,
                 account=self.account,
                 subuser=self.subuser,
                 role=self.role,
@@ -52,14 +55,17 @@ def get_client(self):
         return self._client
 
     def mantash(self, args):
-        mantash = os.path.realpath(
-            os.path.join(os.path.dirname(__file__), "..", "bin", "mantash"))
+        mantash = os.path.realpath(os.path.join(
+            os.path.dirname(__file__), "..", "bin", "mantash"))
         argv = [sys.executable, mantash]
         MANTA_INSECURE = bool(os.environ.get('MANTA_INSECURE', False))
         if MANTA_INSECURE:
             argv.append('-k')
         argv += args
-        p = subprocess.Popen(argv, shell=False, stdout=PIPE, stderr=PIPE,
+        p = subprocess.Popen(argv,
+                             shell=False,
+                             stdout=PIPE,
+                             stderr=PIPE,
                              close_fds=True)
         p.wait()
         stdout = p.stdout.read()
diff --git a/test/test.py b/test/test.py
index 224a6b8..bd7a953 100755
--- a/test/test.py
+++ b/test/test.py
@@ -3,6 +3,8 @@
 
 """The python-manta test suite entry point."""
 
+from __future__ import absolute_import
+
 import os
 from os.path import abspath, dirname
 import sys
diff --git a/test/test_auth.py b/test/test_auth.py
index 7e32d0f..675c61d 100644
--- a/test/test_auth.py
+++ b/test/test_auth.py
@@ -3,6 +3,8 @@
 
 """Test the python-manta auth methods."""
 
+from __future__ import absolute_import
+
 from os.path import abspath, curdir
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives import serialization
@@ -16,7 +18,6 @@
 
 #---- globals
 
-message = "hello"
 DIR = abspath(curdir)
 
 #---- internal support stuff
@@ -84,51 +85,57 @@ def get_hash_class_from_algorithm(algo):
     },
 }
 
-def _sign_message(self, key_name):
-        key = KEYS[key_name]
-        self.assertTrue(key)
-
-        with open(key["file"], 'rb') as f:
-            priv_key = f.read()
-            f.close()
-
-        signer = manta.PrivateKeySigner(key["fp"], priv_key)
-        signed = signer.sign(message)
-        self.assertEqual(len(signed), 3)
-        self.assertTrue(signed[0] == key["sighash"])
-        signature = base64.b64decode(signed[2])
-        hash_class = get_hash_class_from_algorithm(signed[0])
-
-        vkey = serialization.load_pem_private_key(
-            priv_key,
-            password=None,
-            backend=default_backend()
+def _sign_message(self, key_name, message='signme'):
+    key = KEYS[key_name]
+    self.assertTrue(key)
+
+    # We want to be able to pass in either bytes or str data
+    if isinstance(message, str):
+        bmessage = message.encode('utf-8')
+    else:
+        bmessage = message
+
+    with open(key["file"], 'rb') as f:
+        priv_key = f.read()
+        f.close()
+
+    signer = manta.PrivateKeySigner(key["fp"], priv_key)
+    signed = signer.sign(message)
+    self.assertEqual(len(signed), 3)
+    self.assertTrue(signed[0] == key["sighash"])
+
+    signature = base64.b64decode(signed[2])
+    hash_class = get_hash_class_from_algorithm(signed[0])
+    vkey = serialization.load_pem_private_key(
+        priv_key,
+        password=None,
+        backend=default_backend()
+    )
+
+    self.assertTrue(vkey)
+    vkey = vkey.public_key()
+    self.assertTrue(vkey)
+
+    # vkey.verify() raises exception if verification fails
+    if key["type"] == "RSA":
+        verified = vkey.verify(
+            signature,
+            bmessage,
+            padding.PKCS1v15(),
+            hash_class()
+        )
+    elif key["type"] == "ECDSA":
+        verified = vkey.verify(
+            signature,
+            bmessage,
+            ec.ECDSA(hash_class())
         )
+    else:
+        self.assertFalse("Unknown Key Type: {}".format(key["type"]))
 
-        self.assertTrue(vkey)
-        vkey = vkey.public_key()
-        self.assertTrue(vkey)
-
-        # vkey.verify() raises exception if verification fails
-        if key["type"] == "RSA":
-            verified = vkey.verify(
-                signature,
-                message,
-                padding.PKCS1v15(),
-                hash_class()
-            )
-        elif key["type"] == "ECDSA":
-            verified = vkey.verify(
-                signature,
-                message,
-                ec.ECDSA(hash_class())
-            )
-        else:
-            self.assertFalse("Unknown Key Type: {}".format(key["type"]))
-
-        self.assertTrue(verified is None)
-
-        return
+    self.assertTrue(verified is None)
+
+    return
 
 #---- Test cases
 
@@ -157,5 +164,8 @@ def test_sign_and_verify_rsa_sha265(self):
     def test_sign_and_verify_rsa_md5(self):
         _sign_message(self, "RSA-SHA256")
 
+    def test_sign_and_verify_binary_ecdsa_sha256(self):
+        _sign_message(self, "ECDSA-SHA256", message=b'signme')
+
 
 ## TODO: add test cases for SSHAgentSigner and CLISigner.
diff --git a/test/test_mantaclient.py b/test/test_mantaclient.py
index 35c0619..9a5c079 100644
--- a/test/test_mantaclient.py
+++ b/test/test_mantaclient.py
@@ -1,25 +1,22 @@
 #!/usr/bin/env python
 # Copyright (c) 2012 Joyent, Inc.  All rights reserved.
-
 """Test the python-manta MantaClient."""
 
+from __future__ import absolute_import
+from __future__ import print_function
+
 import re
 from posixpath import dirname as udirname, basename as ubasename, join as ujoin
 
 from common import *
 import manta
 
-
-
 #---- globals
 
 TDIR = "tmp/test_mantaclient"
 
-
-
 #---- internal support stuff
 
-
 #---- Test cases
 #
 # We need to run these tests in order. We'll be creating a test area:
@@ -27,8 +24,10 @@
 # and working in there.
 #
 
+
 class MiscTestCase(MantaTestCase):
     """Miscellaneous 'manta' module tests."""
+
     def test_imports(self):
         self.assertTrue(manta.MantaClient)
         self.assertTrue(manta.PrivateKeySigner)
@@ -42,12 +41,13 @@ def test_version(self):
         self.assertTrue(manta.__version__)
         self.assertTrue(VERSION_RE.search(manta.__version__))
 
+
 class CleanTestAreaTestCase(MantaTestCase):
     def test_clean(self):
         client = self.get_client()
         try:
             client.list_directory(stor(TDIR))
-        except manta.MantaError, ex:
+        except manta.MantaError as ex:
             if ex.code == "ResourceNotFound":
                 return
             else:
@@ -62,6 +62,7 @@ def test_clean(self):
                 client.delete_object(ujoin(mdir, nondir["name"]))
             client.delete_object(mdir)
 
+
 class DirTestCase(MantaTestCase):
     def test_put(self):
         client = self.get_client()
@@ -76,12 +77,14 @@ def test_listheaddel(self):
         for d in ['a', 'b', 'c']:
             client.mkdirp(stor(TDIR, 'dir', d))
         dirents = client.list_directory(stor(TDIR, 'dir'))
-        self.assertEqual(len(dirents), 3,
-            'unexpected number of dirents: got %d, expected 3, dirents %r' % (
-                len(dirents), dirents))
+        self.assertEqual(
+            len(dirents), 3,
+            'unexpected number of dirents: got %d, expected 3, dirents %r' %
+            (len(dirents), dirents))
         dirents = client.list_directory(stor(TDIR, 'dir'), limit=2)
         self.assertEqual(len(dirents), 2)
-        dirents = client.list_directory(stor(TDIR, 'dir'), marker=dirents[-1]["name"])
+        dirents = client.list_directory(
+            stor(TDIR, 'dir'), marker=dirents[-1]["name"])
         self.assertEqual(len(dirents), 2)
         self.assertEqual(dirents[1]["name"], "c")
 
@@ -93,6 +96,7 @@ def test_listheaddel(self):
         dirents = client.list_directory(stor(TDIR, 'dir'))
         self.assertEqual(len(dirents), 0)
 
+
 class ObjectTestCase(MantaTestCase):
     def test_putgetdel(self):
         client = self.get_client()
@@ -104,9 +108,10 @@ def test_putgetdel(self):
         self.assertEqual(content, got)
         client.delete_object(mpath)
         dirents = [e for e in client.list_directory(stor(TDIR))
-            if e["name"] == "foo.txt"]
+                   if e["name"] == "foo.txt"]
         self.assertEqual(len(dirents), 0)
 
+
 class LinkTestCase(MantaTestCase):
     def test_put(self):
         client = self.get_client()
@@ -129,7 +134,7 @@ def test_put(self):
         self.assertEqual(content, got2)
         client.delete_object(link_path)
         dirents = [e for e in client.list_directory(stor(TDIR))
-            if e["name"] in ("obj.txt", "link.txt")]
+                   if e["name"] in ("obj.txt", "link.txt")]
         self.assertEqual(len(dirents), 0)
 
 
diff --git a/test/test_mantash.py b/test/test_mantash.py
index 7765bb9..21fc347 100644
--- a/test/test_mantash.py
+++ b/test/test_mantash.py
@@ -1,44 +1,50 @@
 #!/usr/bin/env python
 # Copyright (c) 2012 Joyent, Inc.  All rights reserved.
-
 """Test mantash."""
 
+from __future__ import absolute_import
+
 import re
 from posixpath import join as ujoin
 
 from common import MantaTestCase, stor
 
-
 #---- globals
 
 TDIR = "tmp/test_mantash"
 
-
-
 #---- test cases
 
+
 class OptionsTestCase(MantaTestCase):
     def test_version(self):
         code, stdout, stderr = self.mantash(['--version'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue(re.compile("^mantash \d+\.\d+\.\d+$").search(stdout))
-        self.assertEqual(stderr, "")
+        self.assertEqual(stderr.decode("utf-8"), "")
         self.assertEqual(code, 0)
 
     def test_help(self):
-        no_man_pages = "No manual entry for mlogin\nNo manual entry for msign\n"
+        no_man_page = b"No manual entry for mlogin\nNo manual entry for msign\n"
         code, stdout, stderr = self.mantash(['help'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("mantash help" in stdout)
-        self.assertIn(stderr, ("", no_man_pages))
+        self.assertIn(stderr, (b"", no_man_page))
         self.assertEqual(code, 0)
+
         code, stdout, stderr = self.mantash(['--help'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("mantash help" in stdout)
-        self.assertIn(stderr, ("", no_man_pages))
+        self.assertIn(stderr, (b"", no_man_page))
         self.assertEqual(code, 0)
+
         code, stdout, stderr = self.mantash(['-h'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("mantash help" in stdout)
-        self.assertIn(stderr, ("", no_man_pages))
+        self.assertIn(stderr, (b"", no_man_page))
         self.assertEqual(code, 0)
 
+
 class FindTestCase(MantaTestCase):
     def setUp(self):
         self.client = self.get_client()
@@ -48,26 +54,34 @@ def setUp(self):
 
     def test_empty(self):
         code, stdout, stderr = self.mantash(['-C', self.base, 'find'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("obj1.txt" in stdout)
         self.assertTrue("dir2" in stdout)
         self.assertEqual(code, 0)
 
     def test_type(self):
-        code, stdout, stderr = self.mantash(['-C', self.base, 'find', '-type', 'f'])
-        self.assertTrue("obj1.txt" in stdout)
-        self.assertTrue("dir2" not in stdout)
+        code, stdout, stderr = self.mantash(
+            ['-C', self.base, 'find', '-type', 'f'])
+        stdout = stdout.decode("utf-8")
+        self.assertTrue(u"obj1.txt" in stdout)
+        self.assertTrue(u"dir2" not in stdout)
         self.assertEqual(code, 0)
 
-        code, stdout, stderr = self.mantash(['-C', self.base, 'find', '-type', 'o'])
+        code, stdout, stderr = self.mantash(
+            ['-C', self.base, 'find', '-type', 'o'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("obj1.txt" in stdout)
         self.assertTrue("dir2" not in stdout)
         self.assertEqual(code, 0)
 
-        code, stdout, stderr = self.mantash(['-C', self.base, 'find', '-type', 'd'])
+        code, stdout, stderr = self.mantash(
+            ['-C', self.base, 'find', '-type', 'd'])
+        stdout = stdout.decode("utf-8")
         self.assertTrue("obj1.txt" not in stdout)
         self.assertTrue("dir2" in stdout)
         self.assertEqual(code, 0)
 
+
 class LsTestCase(MantaTestCase):
     def setUp(self):
         self.client = self.get_client()
@@ -81,10 +95,12 @@ def setUp(self):
 
     def test_bare(self):
         code, stdout, stderr = self.mantash(['-C', self.base, 'ls'])
+        stdout = stdout.decode("utf-8")
         self.assertEqual(stdout, 'a1\nb1\nc1.txt\n')
         self.assertEqual(code, 0)
 
         code, stdout, stderr = self.mantash(['-C', self.base, 'ls', '-l'])
+        stdout = stdout.decode("utf-8")
         lines = stdout.splitlines()
         self.assertEqual(len(lines), 3)
         for line in lines:
@@ -92,15 +108,18 @@ def test_bare(self):
         self.assertEqual(code, 0)
 
         code, stdout, stderr = self.mantash(['-C', self.base, 'ls', '-F'])
+        stdout = stdout.decode("utf-8")
         self.assertEqual(stdout, 'a1/\nb1/\nc1.txt\n')
         self.assertEqual(code, 0)
 
     def test_dir(self):
         code, stdout, stderr = self.mantash(['-C', self.base, 'ls', 'a1'])
+        stdout = stdout.decode("utf-8")
         self.assertEqual(stdout, 'a2.txt\nb2\n')
         self.assertEqual(code, 0)
 
     def test_dirstar(self):
         code, stdout, stderr = self.mantash(['-C', self.base, 'ls', 'a1/*'])
+        stdout = stdout.decode("utf-8")
         self.assertEqual(stdout, 'a1/a2.txt\n\na1/b2:\na3.txt\n')
         self.assertEqual(code, 0)
diff --git a/test/testall.py b/test/testall.py
index 882b73d..637d4e3 100644
--- a/test/testall.py
+++ b/test/testall.py
@@ -4,6 +4,9 @@
 # Run the test suite against all the Python versions we can find.
 #
 
+from __future__ import absolute_import
+from __future__ import print_function
+
 import sys
 import os
 from os.path import dirname, abspath, join
@@ -25,8 +28,7 @@ def _python_ver_from_python(python):
 
 def _gen_python_names():
     yield "python"
-    for ver in [(2,2), (2,3), (2,4), (2,5), (2,6), (2,7), (3,0), (3,1),
-                (3,2), (3,3)]:
+    for ver in [(2,6), (2,7), (3,3), (3,4), (3,5), (3,6), (3,7)]:
         yield "python%d.%d" % ver
         if sys.platform == "win32":
             yield "python%d%d" % ver
@@ -47,7 +49,7 @@ def testall():
             # Don't support Python < 2.3.
             continue
         ver_str = "%s.%s" % ver
-        print "-- test with Python %s (%s)" % (ver_str, python)
+        print ("-- test with Python %s (%s)" % (ver_str, python))
         assert ' ' not in python
         test_py = join(TOP, "test", "test.py")
         rv = os.system("%s %s -- -knownfailure" % (python, test_py))
diff --git a/test/testlib.py b/test/testlib.py
index 6c7bc3a..868f60b 100644
--- a/test/testlib.py
+++ b/test/testlib.py
@@ -49,6 +49,9 @@
 # - See the optparse "TODO" below.
 # - Make the quiet option actually quiet.
 
+from __future__ import absolute_import
+from __future__ import print_function
+
 __version_info__ = (0, 6, 6)
 __version__ = '.'.join(map(str, __version_info__))