Repository: wearpants/twiggy

Issue:
0.4.x release with Python3 support
I'm testing twiggy on an isolated environment with `Python3.4` using `virtualenvwrapper`, here's the scenario:

## Creating the isolated environment:
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ mkproject -p /usr/bin/python3 twiggy
Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in twiggy/bin/python3
Also creating executable in twiggy/bin/python
Installing setuptools, pip...done.
Creating /home/username/Devel/twiggy
Setting project for twiggy to /home/username/Devel/twiggy
```

## Installing `twiggy`:
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ pip install twiggy
Downloading/unpacking twiggy
  Downloading Twiggy-0.4.5.tar.gz (55kB): 55kB downloaded
  Running setup.py (path:/home/username/.virtualenvs/twiggy/build/twiggy/setup.py) egg_info for package twiggy
    
Installing collected packages: twiggy
  Running setup.py install for twiggy
    
Successfully installed twiggy
Cleaning up...
```

## Testing w/ Python3 interpreter:
It first complains about a missing module (`logger`).
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ python
Python 3.4.0 (default, Apr 11 2014, 13:05:11) 
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import twiggy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/username/.virtualenvs/twiggy/lib/python3.4/site-packages/twiggy/__init__.py", line 6, in <module>
    import logger
ImportError: No module named 'logger'
>>> exit()
```

## Let's install that `logger`:
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ pip install logger
Downloading/unpacking logger
  Downloading logger-1.4.tar.gz
  Running setup.py (path:/home/username/.virtualenvs/twiggy/build/logger/setup.py) egg_info for package logger
    
Installing collected packages: logger
  Running setup.py install for logger
    
Successfully installed logger
Cleaning up...
```

## Let's test it again:
This time, it will complain about a missing module: `filters`
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ python
Python 3.4.0 (default, Apr 11 2014, 13:05:11) 
[GCC 4.8.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> pip install twiggy
  File "<stdin>", line 1
    pip install twiggy
              ^
SyntaxError: invalid syntax
>>> import twiggy
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/username/.virtualenvs/twiggy/lib/python3.4/site-packages/twiggy/__init__.py", line 7, in <module>
    import filters
ImportError: No module named 'filters'
>>> exit()
```

## Let's try to install that missing module:
```
(twiggy) username@username-VirtualBox ~/Devel/twiggy 
$ pip install filters
Downloading/unpacking filters
  Could not find any downloads that satisfy the requirement filters
Cleaning up...
No distributions at all found for filters
Storing debug log for failure in /home/username/.pip/pip.log
```

Now we're stuck :)

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..28e0a81
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: python
+
+sudo: false
+env:
+  - TOXENV=py26
+  - TOXENV=py27
+  - TOXENV=py33
+  - TOXENV=py34
+  - TOXENV=pypy
+
+install:
+  - travis_retry pip install tox
+
+script:
+  - tox
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2e98e9e
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,19 @@
+# Tox (http://tox.testrun.org/) is a tool for running tests
+# in multiple virtualenvs. This configuration file will run the
+# test suite on all supported python versions. To use it, "pip install tox"
+# and then run "tox" from this directory.
+
+[tox]
+envlist = py26, py27, pypy, py33, py34
+
+[testenv]
+setenv =
+    TWIGGY_UNDER_TEST=1
+commands = py.test {posargs:--tb=short tests/}
+deps =
+    pytest
+
+[testenv:py26]
+deps =
+    pytest
+    unittest2
diff --git a/twiggy/__init__.py b/twiggy/__init__.py
index 56c73a4..0ae6a14 100644
--- a/twiggy/__init__.py
+++ b/twiggy/__init__.py
@@ -3,11 +3,11 @@ import time
 import sys
 import os
 
-import logger
-import filters
-import formats
-import outputs
-import levels
+from . import logger
+from . import filters
+from . import formats
+from . import outputs
+from . import levels
 
 
 ## globals creation is wrapped in a function so that we can do sane testing
diff --git a/twiggy/compat.py b/twiggy/compat.py
new file mode 100644
index 0000000..6bc97c4
--- /dev/null
+++ b/twiggy/compat.py
@@ -0,0 +1,652 @@
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+# Copyright (c) 2010-2014 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson <benjamin@python.org>"
+__version__ = "1.6.1"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    string_types = str,
+    integer_types = int,
+    class_types = type,
+    text_type = str
+    binary_type = bytes
+
+    MAXSIZE = sys.maxsize
+else:
+    string_types = basestring,
+    integer_types = (int, long)
+    class_types = (type, types.ClassType)
+    text_type = unicode
+    binary_type = str
+
+    if sys.platform.startswith("java"):
+        # Jython always uses 32 bits.
+        MAXSIZE = int((1 << 31) - 1)
+    else:
+        # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+        class X(object):
+            def __len__(self):
+                return 1 << 31
+        try:
+            len(X())
+        except OverflowError:
+            # 32-bit
+            MAXSIZE = int((1 << 31) - 1)
+        else:
+            # 64-bit
+            MAXSIZE = int((1 << 63) - 1)
+        del X
+
+
+def _add_doc(func, doc):
+    """Add documentation to a function."""
+    func.__doc__ = doc
+
+
+def _import_module(name):
+    """Import module, returning the module after the last dot."""
+    __import__(name)
+    return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+    def __init__(self, name):
+        self.name = name
+
+    def __get__(self, obj, tp):
+        try:
+            result = self._resolve()
+        except ImportError:
+            # See the nice big comment in MovedModule.__getattr__.
+            raise AttributeError("%s could not be imported " % self.name)
+        setattr(obj, self.name, result) # Invokes __set__.
+        # This is a bit ugly, but it avoids running this again.
+        delattr(obj.__class__, self.name)
+        return result
+
+
+class MovedModule(_LazyDescr):
+
+    def __init__(self, name, old, new=None):
+        super(MovedModule, self).__init__(name)
+        if PY3:
+            if new is None:
+                new = name
+            self.mod = new
+        else:
+            self.mod = old
+
+    def _resolve(self):
+        return _import_module(self.mod)
+
+    def __getattr__(self, attr):
+        # It turns out many Python frameworks like to traverse sys.modules and
+        # try to load various attributes. This causes problems if this is a
+        # platform-specific module on the wrong platform, like _winreg on
+        # Unixes. Therefore, we silently pretend unimportable modules do not
+        # have any attributes. See issues #51, #53, #56, and #63 for the full
+        # tales of woe.
+        #
+        # First, if possible, avoid loading the module just to look at __file__,
+        # __name__, or __path__.
+        if (attr in ("__file__", "__name__", "__path__") and
+            self.mod not in sys.modules):
+            raise AttributeError(attr)
+        try:
+            _module = self._resolve()
+        except ImportError:
+            raise AttributeError(attr)
+        value = getattr(_module, attr)
+        setattr(self, attr, value)
+        return value
+
+
+class _LazyModule(types.ModuleType):
+
+    def __init__(self, name):
+        super(_LazyModule, self).__init__(name)
+        self.__doc__ = self.__class__.__doc__
+
+    def __dir__(self):
+        attrs = ["__doc__", "__name__"]
+        attrs += [attr.name for attr in self._moved_attributes]
+        return attrs
+
+    # Subclasses should override this
+    _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+    def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+        super(MovedAttribute, self).__init__(name)
+        if PY3:
+            if new_mod is None:
+                new_mod = name
+            self.mod = new_mod
+            if new_attr is None:
+                if old_attr is None:
+                    new_attr = name
+                else:
+                    new_attr = old_attr
+            self.attr = new_attr
+        else:
+            self.mod = old_mod
+            if old_attr is None:
+                old_attr = name
+            self.attr = old_attr
+
+    def _resolve(self):
+        module = _import_module(self.mod)
+        return getattr(module, self.attr)
+
+
+
+class _MovedItems(_LazyModule):
+    """Lazy loading of moved objects"""
+
+
+_moved_attributes = [
+    MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+    MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+    MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+    MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+    MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+    MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+    MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
+    MovedAttribute("reduce", "__builtin__", "functools"),
+    MovedAttribute("StringIO", "StringIO", "io"),
+    MovedAttribute("UserString", "UserString", "collections"),
+    MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+    MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+    MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+
+    MovedModule("builtins", "__builtin__"),
+    MovedModule("configparser", "ConfigParser"),
+    MovedModule("copyreg", "copy_reg"),
+    MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+    MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+    MovedModule("http_cookies", "Cookie", "http.cookies"),
+    MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+    MovedModule("html_parser", "HTMLParser", "html.parser"),
+    MovedModule("http_client", "httplib", "http.client"),
+    MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+    MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+    MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+    MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+    MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+    MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+    MovedModule("cPickle", "cPickle", "pickle"),
+    MovedModule("queue", "Queue"),
+    MovedModule("reprlib", "repr"),
+    MovedModule("socketserver", "SocketServer"),
+    MovedModule("_thread", "thread", "_thread"),
+    MovedModule("tkinter", "Tkinter"),
+    MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+    MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+    MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+    MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+    MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+    MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+    MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+    MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+    MovedModule("tkinter_colorchooser", "tkColorChooser",
+                "tkinter.colorchooser"),
+    MovedModule("tkinter_commondialog", "tkCommonDialog",
+                "tkinter.commondialog"),
+    MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+    MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+    MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+    MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+                "tkinter.simpledialog"),
+    MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+    MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+    MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+    MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+    MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+    MovedModule("xmlrpc_server", "xmlrpclib", "xmlrpc.server"),
+    MovedModule("winreg", "_winreg"),
+]
+for attr in _moved_attributes:
+    setattr(_MovedItems, attr.name, attr)
+    if isinstance(attr, MovedModule):
+        sys.modules[__name__ + ".moves." + attr.name] = attr
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = sys.modules[__name__ + ".moves"] = _MovedItems(__name__ + ".moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+    """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+    MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+    MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+    MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+    MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+    MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+    MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+    MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+    MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+    MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+    MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+    MovedAttribute("quote", "urllib", "urllib.parse"),
+    MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+    MovedAttribute("unquote", "urllib", "urllib.parse"),
+    MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+    MovedAttribute("urlencode", "urllib", "urllib.parse"),
+    MovedAttribute("splitquery", "urllib", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+    setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+sys.modules[__name__ + ".moves.urllib_parse"] = sys.modules[__name__ + ".moves.urllib.parse"] = Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+    """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+    MovedAttribute("URLError", "urllib2", "urllib.error"),
+    MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+    MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+    setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+sys.modules[__name__ + ".moves.urllib_error"] = sys.modules[__name__ + ".moves.urllib.error"] = Module_six_moves_urllib_error(__name__ + ".moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+    """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+    MovedAttribute("urlopen", "urllib2", "urllib.request"),
+    MovedAttribute("install_opener", "urllib2", "urllib.request"),
+    MovedAttribute("build_opener", "urllib2", "urllib.request"),
+    MovedAttribute("pathname2url", "urllib", "urllib.request"),
+    MovedAttribute("url2pathname", "urllib", "urllib.request"),
+    MovedAttribute("getproxies", "urllib", "urllib.request"),
+    MovedAttribute("Request", "urllib2", "urllib.request"),
+    MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+    MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+    MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+    MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+    MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+    MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+    MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+    MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+    MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+    MovedAttribute("URLopener", "urllib", "urllib.request"),
+    MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+    MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+    setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+sys.modules[__name__ + ".moves.urllib_request"] = sys.modules[__name__ + ".moves.urllib.request"] = Module_six_moves_urllib_request(__name__ + ".moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+    """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+    MovedAttribute("addbase", "urllib", "urllib.response"),
+    MovedAttribute("addclosehook", "urllib", "urllib.response"),
+    MovedAttribute("addinfo", "urllib", "urllib.response"),
+    MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+    setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+sys.modules[__name__ + ".moves.urllib_response"] = sys.modules[__name__ + ".moves.urllib.response"] = Module_six_moves_urllib_response(__name__ + ".moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+    """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+    MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+    setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+sys.modules[__name__ + ".moves.urllib_robotparser"] = sys.modules[__name__ + ".moves.urllib.robotparser"] = Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+    """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+    parse = sys.modules[__name__ + ".moves.urllib_parse"]
+    error = sys.modules[__name__ + ".moves.urllib_error"]
+    request = sys.modules[__name__ + ".moves.urllib_request"]
+    response = sys.modules[__name__ + ".moves.urllib_response"]
+    robotparser = sys.modules[__name__ + ".moves.urllib_robotparser"]
+
+    def __dir__(self):
+        return ['parse', 'error', 'request', 'response', 'robotparser']
+
+
+sys.modules[__name__ + ".moves.urllib"] = Module_six_moves_urllib(__name__ + ".moves.urllib")
+
+
+def add_move(move):
+    """Add an item to six.moves."""
+    setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+    """Remove item from six.moves."""
+    try:
+        delattr(_MovedItems, name)
+    except AttributeError:
+        try:
+            del moves.__dict__[name]
+        except KeyError:
+            raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+    _meth_func = "__func__"
+    _meth_self = "__self__"
+
+    _func_closure = "__closure__"
+    _func_code = "__code__"
+    _func_defaults = "__defaults__"
+    _func_globals = "__globals__"
+else:
+    _meth_func = "im_func"
+    _meth_self = "im_self"
+
+    _func_closure = "func_closure"
+    _func_code = "func_code"
+    _func_defaults = "func_defaults"
+    _func_globals = "func_globals"
+
+
+try:
+    advance_iterator = next
+except NameError:
+    def advance_iterator(it):
+        return it.next()
+next = advance_iterator
+
+
+try:
+    callable = callable
+except NameError:
+    def callable(obj):
+        return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+    def get_unbound_function(unbound):
+        return unbound
+
+    create_bound_method = types.MethodType
+
+    Iterator = object
+else:
+    def get_unbound_function(unbound):
+        return unbound.im_func
+
+    def create_bound_method(func, obj):
+        return types.MethodType(func, obj, obj.__class__)
+
+    class Iterator(object):
+
+        def next(self):
+            return type(self).__next__(self)
+
+    callable = callable
+_add_doc(get_unbound_function,
+         """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+    def iterkeys(d, **kw):
+        return iter(d.keys(**kw))
+
+    def itervalues(d, **kw):
+        return iter(d.values(**kw))
+
+    def iteritems(d, **kw):
+        return iter(d.items(**kw))
+
+    def iterlists(d, **kw):
+        return iter(d.lists(**kw))
+else:
+    def iterkeys(d, **kw):
+        return iter(d.iterkeys(**kw))
+
+    def itervalues(d, **kw):
+        return iter(d.itervalues(**kw))
+
+    def iteritems(d, **kw):
+        return iter(d.iteritems(**kw))
+
+    def iterlists(d, **kw):
+        return iter(d.iterlists(**kw))
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+         "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+         "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+    def b(s):
+        return s.encode("latin-1")
+    def u(s):
+        return s
+    unichr = chr
+    if sys.version_info[1] <= 1:
+        def int2byte(i):
+            return bytes((i,))
+    else:
+        # This is about 2x faster than the implementation above on 3.2+
+        int2byte = operator.methodcaller("to_bytes", 1, "big")
+    byte2int = operator.itemgetter(0)
+    indexbytes = operator.getitem
+    iterbytes = iter
+    import io
+    StringIO = io.StringIO
+    BytesIO = io.BytesIO
+else:
+    def b(s):
+        return s
+    # Workaround for standalone backslash
+    def u(s):
+        return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+    unichr = unichr
+    int2byte = chr
+    def byte2int(bs):
+        return ord(bs[0])
+    def indexbytes(buf, i):
+        return ord(buf[i])
+    def iterbytes(buf):
+        return (ord(byte) for byte in buf)
+    import StringIO
+    StringIO = BytesIO = StringIO.StringIO
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+if PY3:
+    exec_ = getattr(moves.builtins, "exec")
+
+
+    def reraise(tp, value, tb=None):
+        if value.__traceback__ is not tb:
+            raise value.with_traceback(tb)
+        raise value
+
+else:
+    def exec_(_code_, _globs_=None, _locs_=None):
+        """Execute code in a namespace."""
+        if _globs_ is None:
+            frame = sys._getframe(1)
+            _globs_ = frame.f_globals
+            if _locs_ is None:
+                _locs_ = frame.f_locals
+            del frame
+        elif _locs_ is None:
+            _locs_ = _globs_
+        exec("""exec _code_ in _globs_, _locs_""")
+
+
+    exec_("""def reraise(tp, value, tb=None):
+    raise tp, value, tb
+""")
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+    def print_(*args, **kwargs):
+        """The new-style print function for Python 2.4 and 2.5."""
+        fp = kwargs.pop("file", sys.stdout)
+        if fp is None:
+            return
+        def write(data):
+            if not isinstance(data, basestring):
+                data = str(data)
+            # If the file has an encoding, encode unicode with it.
+            if (isinstance(fp, file) and
+                isinstance(data, unicode) and
+                fp.encoding is not None):
+                errors = getattr(fp, "errors", None)
+                if errors is None:
+                    errors = "strict"
+                data = data.encode(fp.encoding, errors)
+            fp.write(data)
+        want_unicode = False
+        sep = kwargs.pop("sep", None)
+        if sep is not None:
+            if isinstance(sep, unicode):
+                want_unicode = True
+            elif not isinstance(sep, str):
+                raise TypeError("sep must be None or a string")
+        end = kwargs.pop("end", None)
+        if end is not None:
+            if isinstance(end, unicode):
+                want_unicode = True
+            elif not isinstance(end, str):
+                raise TypeError("end must be None or a string")
+        if kwargs:
+            raise TypeError("invalid keyword arguments to print()")
+        if not want_unicode:
+            for arg in args:
+                if isinstance(arg, unicode):
+                    want_unicode = True
+                    break
+        if want_unicode:
+            newline = unicode("\n")
+            space = unicode(" ")
+        else:
+            newline = "\n"
+            space = " "
+        if sep is None:
+            sep = space
+        if end is None:
+            end = newline
+        for i, arg in enumerate(args):
+            if i:
+                write(sep)
+            write(arg)
+        write(end)
+
+_add_doc(reraise, """Reraise an exception.""")
+
+
+def with_metaclass(meta, *bases):
+    """Create a base class with a metaclass."""
+    return meta("NewBase", bases, {})
+
+def add_metaclass(metaclass):
+    """Class decorator for creating a class with a metaclass."""
+    def wrapper(cls):
+        orig_vars = cls.__dict__.copy()
+        orig_vars.pop('__dict__', None)
+        orig_vars.pop('__weakref__', None)
+        slots = orig_vars.get('__slots__')
+        if slots is not None:
+            if isinstance(slots, str):
+                slots = [slots]
+            for slots_var in slots:
+                orig_vars.pop(slots_var)
+        return metaclass(cls.__name__, cls.__bases__, orig_vars)
+    return wrapper
diff --git a/twiggy/filters.py b/twiggy/filters.py
index 30247d3..d05890b 100644
--- a/twiggy/filters.py
+++ b/twiggy/filters.py
@@ -1,7 +1,10 @@
-import levels
 import fnmatch
 import re
 
+from . import levels
+from . import compat
+
+
 __re_type = type(re.compile('foo')) # XXX is there a canonical place for this?
 
 def msgFilter(x):
@@ -11,7 +14,7 @@ def msgFilter(x):
         return lambda msg: True
     elif isinstance(x, bool):
         return lambda msg: x
-    elif isinstance(x, basestring):
+    elif isinstance(x, compat.string_types):
         return regex_wrapper(re.compile(x))
     elif isinstance(x, __re_type):
         return regex_wrapper(x)
diff --git a/twiggy/lib/converter.py b/twiggy/lib/converter.py
index 3aeff8b..32199ce 100644
--- a/twiggy/lib/converter.py
+++ b/twiggy/lib/converter.py
@@ -1,5 +1,8 @@
 import copy
 
+from twiggy.compat import iterkeys
+
+
 class Converter(object):
     """Holder for `.ConversionTable` items
 
@@ -69,7 +72,7 @@ class ConversionTable(list):
         # XXX I could be much faster & efficient!
         # XXX I have written this pattern at least 10 times
         converts = set(x.key for x in self)
-        avail = set(d.iterkeys())
+        avail = set(iterkeys(d))
         required = set(x.key for x in self if x.required)
         missing = required - avail
 
diff --git a/twiggy/logger.py b/twiggy/logger.py
index 81fdce9..41c5687 100644
--- a/twiggy/logger.py
+++ b/twiggy/logger.py
@@ -1,9 +1,11 @@
+from __future__ import print_function
 from .message import Message
 from .lib import iso8601time
 import twiggy as _twiggy
-import levels
-import outputs
-import formats
+from . import levels
+from . import outputs
+from . import formats
+from .compat import iteritems
 
 import warnings
 import sys
@@ -11,6 +13,8 @@ import time
 import traceback
 from functools import wraps
 
+StandardError = Exception
+
 def emit(level):
     """a decorator that emits at `level <.LogLevel>` after calling the method. The method
     should return a `.Logger` instance.
@@ -138,8 +142,8 @@ class InternalLogger(BaseLogger):
             else:
                 self.output.output(msg)
         except StandardError:
-            print>>sys.stderr, iso8601time(), "Error in twiggy internal log! Something is serioulsy broken."
-            print>>sys.stderr, "Offending message:", repr(msg)
+            print(iso8601time(), "Error in twiggy internal log! Something is serioulsy broken.", file=sys.stderr)
+            print("Offending message:", repr(msg), file=sys.stderr)
             traceback.print_exc(file = sys.stderr)
 
 class Logger(BaseLogger):
@@ -229,7 +233,7 @@ class Logger(BaseLogger):
             # just continue emitting in face of filter error
 
         # XXX should we trap here too b/c of "Dictionary changed size during iteration" (or other rare errors?)
-        potential_emitters = [(name, emitter) for name, emitter in self._emitters.iteritems()
+        potential_emitters = [(name, emitter) for name, emitter in iteritems(self._emitters)
                               if level >= emitter.min_level]
 
         if not potential_emitters: return
diff --git a/twiggy/message.py b/twiggy/message.py
index a40e41e..5879b7b 100644
--- a/twiggy/message.py
+++ b/twiggy/message.py
@@ -4,6 +4,9 @@ import sys
 import traceback
 from string import Template
 
+from .compat import iteritems
+
+
 class Message(object):
     """A log message.  All attributes are read-only."""
 
@@ -60,11 +63,11 @@ class Message(object):
         ## and substituting into `format_spec`.
 
         ## call any callables
-        for k, v in fields.iteritems():
+        for k, v in iteritems(fields):
             if callable(v):
                 fields[k] = v()
 
-        for k, v in kwargs.iteritems():
+        for k, v in iteritems(kwargs):
             if callable(v):
                 kwargs[k] = v()
 


Test patch:
diff --git a/tests/test_globals.py b/tests/test_globals.py
index 94fff04..3185c2c 100644
--- a/tests/test_globals.py
+++ b/tests/test_globals.py
@@ -80,7 +80,7 @@ class GlobalsTestCase(unittest.TestCase):
     def test_quickSetup_file(self):
 
         fname = tempfile.mktemp()
-        print fname
+        print(fname)
         
         @self.addCleanup      
         def cleanup():
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 3f19f05..4d99b49 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 import sys
 if sys.version_info >= (2, 7):
     import unittest
@@ -7,10 +8,10 @@ else:
     except ImportError:
         raise RuntimeError("unittest2 is required for Python < 2.7")
 import twiggy
-import StringIO
 import time
 
 from . import when
+from twiggy.compat import StringIO
 
 def fake_gmtime():
     return when
@@ -25,9 +26,9 @@ class IntegrationTestCase(unittest.TestCase):
         twiggy._del_globals()
 
     def test_integration(self):
-        everything = twiggy.outputs.StreamOutput(stream=StringIO.StringIO(), format=twiggy.formats.line_format)
-        out1 = twiggy.outputs.StreamOutput(stream=StringIO.StringIO(), format=twiggy.formats.line_format)
-        out2 = twiggy.outputs.StreamOutput(stream=StringIO.StringIO(), format=twiggy.formats.line_format)
+        everything = twiggy.outputs.StreamOutput(stream=StringIO(), format=twiggy.formats.line_format)
+        out1 = twiggy.outputs.StreamOutput(stream=StringIO(), format=twiggy.formats.line_format)
+        out2 = twiggy.outputs.StreamOutput(stream=StringIO(), format=twiggy.formats.line_format)
         
         twiggy.addEmitters(('*', twiggy.levels.DEBUG, None, everything),
                            ('first', twiggy.levels.INFO, None, out1),
@@ -48,13 +49,13 @@ class IntegrationTestCase(unittest.TestCase):
         except:
             twiggy.log.trace().critical("Went boom")
 
-        print "***************** everything **********************"
-        print everything.stream.getvalue(),
-        print "****************** out 1 **************************"
-        print out1.stream.getvalue(),
-        print "****************** out 2 **************************"
-        print out2.stream.getvalue(),       
-        print "***************************************************"
+        print("***************** everything **********************")
+        print(everything.stream.getvalue(), end=' ')
+        print("****************** out 1 **************************")
+        print(out1.stream.getvalue(), end=' ')
+        print("****************** out 2 **************************")
+        print(out2.stream.getvalue(), end=' ')       
+        print("***************************************************")
         
 
         # XXX this should really be done with a regex, but I'm feeling lazy
diff --git a/tests/test_logger.py b/tests/test_logger.py
index 2aecd7e..f2b1970 100644
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -1,3 +1,4 @@
+import re
 import sys
 if sys.version_info >= (2, 7):
     import unittest
@@ -7,9 +8,9 @@ else:
     except ImportError:
         raise RuntimeError("unittest2 is required for Python < 2.7")
 import sys
-import StringIO
 
 from twiggy import logger, outputs, levels, filters
+from twiggy.compat import StringIO
 import twiggy as _twiggy
 
 class LoggerTestBase(object):
@@ -125,7 +126,7 @@ class InternalLoggerTest(LoggerTestBase, unittest.TestCase):
         assert log.min_level == self.log.min_level
 
     def test_trap_msg(self):
-        sio = StringIO.StringIO()
+        sio = StringIO()
 
         def cleanup(stderr):
             sys.stderr = stderr
@@ -152,7 +153,7 @@ class InternalLoggerTest(LoggerTestBase, unittest.TestCase):
 
         out = BorkedOutput(close_atexit = False)
 
-        sio = StringIO.StringIO()
+        sio = StringIO()
 
         def cleanup(stderr, output):
             sys.stderr = stderr
@@ -270,16 +271,16 @@ class LoggerTrapTestCase(unittest.TestCase):
         assert len(self.internal_messages) == 1
         m = self.internal_messages.pop()
         
-        print m.text
-        print m.traceback
-        print _twiggy.internal_log._options
+        print(m.text)
+        print(m.traceback)
+        print(_twiggy.internal_log._options)
         
         assert m.level == levels.INFO
         assert m.name == 'twiggy.internal'
         assert "Traceback" in m.traceback
         assert "THUNK" in m.traceback
         assert "Error in Logger filtering" in m.text
-        assert "<function bad_filter" in m.text
+        assert re.search("<function .*bad_filter", m.text)
                 
     def test_trap_bad_msg(self):
         def go_boom():
@@ -291,16 +292,16 @@ class LoggerTrapTestCase(unittest.TestCase):
         assert len(self.internal_messages) == 1
         m = self.internal_messages.pop()
         
-        print m.text
-        print m.traceback
-        print _twiggy.internal_log._options
+        print(m.text)
+        print(m.traceback)
+        print(_twiggy.internal_log._options)
         
         assert m.level == levels.INFO
         assert m.name == 'twiggy.internal'
         assert "Traceback" in m.traceback
         assert "BOOM" in m.traceback
         assert "Error formatting message" in m.text
-        assert "<function go_boom" in m.text
+        assert re.search("<function .*go_boom", m.text)
     
     def test_trap_output(self):
         class BorkedOutput(outputs.ListOutput):
@@ -331,11 +332,13 @@ class LoggerTrapTestCase(unittest.TestCase):
         assert len(self.internal_messages) == 1
         m = self.internal_messages.pop()
 
-        print m.text
-        print m.traceback
+        print(m.text)
+        print(m.traceback)
 
         assert m.level == levels.WARNING
-        assert "Error outputting with <tests.test_logger.BorkedOutput" in m.text
+        assert re.search(
+            "Error outputting with <tests.test_logger.*BorkedOutput",
+            m.text)
         assert "Traceback" in m.traceback
         assert "BORK" in m.traceback
         
@@ -375,12 +378,12 @@ class LoggerTrapTestCase(unittest.TestCase):
         assert len(self.internal_messages) == 1
         m = self.internal_messages.pop()
 
-        print m.text
-        print m.traceback
+        print(m.text)
+        print(m.traceback)
 
         assert m.level == levels.INFO
         assert "Error filtering with emitter before" in m.text
-        assert "<function go_boom" in m.text
+        assert re.search("<function .*go_boom", m.text)
         assert "Traceback" in m.traceback
         assert "BOOM" in m.traceback
         
diff --git a/tests/test_message.py b/tests/test_message.py
index 162a030..73e4e36 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -258,7 +258,7 @@ and shirt'''
                 )
 
         assert m.traceback.startswith('Traceback (most recent call last):')
-        assert m.traceback.endswith('ZeroDivisionError: integer division or modulo by zero\n')
+        assert 'ZeroDivisionError:' in m.traceback
 
     def test_trace_tuple(self):
         opts = Message._default_options.copy()
@@ -276,4 +276,4 @@ and shirt'''
                 )
 
         assert m.traceback.startswith('Traceback (most recent call last):')
-        assert m.traceback.endswith('ZeroDivisionError: integer division or modulo by zero\n')
+        assert 'ZeroDivisionError:' in m.traceback
diff --git a/tests/test_outputs.py b/tests/test_outputs.py
index 766058f..f8f4414 100644
--- a/tests/test_outputs.py
+++ b/tests/test_outputs.py
@@ -8,9 +8,9 @@ else:
         raise RuntimeError("unittest2 is required for Python < 2.7")
 import tempfile
 import os
-import StringIO
 
 from twiggy import outputs, formats
+from twiggy.compat import StringIO
 
 from . import make_mesg, when
 
@@ -41,7 +41,7 @@ class FileOutputTestCase(unittest.TestCase):
     def make_output(self, msg_buffer, locked):
         cls = outputs.FileOutput if locked else UnlockedFileOutput
         
-        return cls(name = self.fname, format = formats.shell_format, buffering = 0,
+        return cls(name = self.fname, format = formats.shell_format, buffering = 1,
                    msg_buffer = msg_buffer, close_atexit=False)
                                          
     def test_sync(self):
@@ -75,7 +75,7 @@ class FileOutputTestCase(unittest.TestCase):
 class StreamOutputTest(unittest.TestCase):
     
     def test_stream_output(self):
-        sio = StringIO.StringIO()
+        sio = StringIO()
         o = outputs.StreamOutput(formats.shell_format, sio)
         o.output(m)
         o.close()