Update project setup.
diff --git a/.coveragerc b/.coveragerc
index 418c1f1..82a7f6c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,19 +1,29 @@
+[run]
+branch = True
+timid = True
+source =
+    .
+omit =
+    .tox/*
+    /usr/*
+    */tmp*
+    setup.py
+
 [report]
 exclude_lines =
-    # Don't complain about defensive assertions
-    raise NotImplementedError
-    raise AssertionError
+    # Have to re-enable the standard pragma
+    \#\s*pragma: no cover
 
-    # Don't complain about non-runnable code
-    if __name__ == .__main__.:
+    # Don't complain if tests don't hit defensive assertion code:
+    ^\s*raise AssertionError\b
+    ^\s*raise NotImplementedError\b
+    ^\s*return NotImplemented\b
+    ^\s*raise$
 
-omit =
-    /usr/*
-    py_env/*
-    */__init__.py
+    # Don't complain if non-runnable code isn't run:
+    ^if __name__ == ['"]__main__['"]:$
 
-    # Ignore test coverage
-    tests/*
+[html]
+directory = coverage-html
 
-    # Don't complain about our pre-commit file
-    pre-commit.py
+# vim:ft=dosini
diff --git a/.gitignore b/.gitignore
index abd9e46..a63d861 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,12 +1,14 @@
-*.pyc
-.pydevproject
-.project
-.coverage
-/py_env
-*.db
-.idea
-build
-dist
 *.egg-info
 *.iml
+*.py[co]
+.*.sw[a-z]
+.coverage
+.idea
 .pre-commit-files
+.project
+.pydevproject
+.tox
+.venv.touch
+/venv*
+coverage-html
+dist
diff --git a/.travis.yml b/.travis.yml
index 51f507a..0688c6d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
 language: python
-
-python:
-    - 2.6
-    - 2.7
-
-install: pip install virtualenv
-script: make coverage
+env: # These should match the tox env list
+    - TOXENV=py26
+    - TOXENV=py27
+    - TOXENV=py33
+    - TOXENV=pypy
+install: pip install tox --use-mirrors
+script: tox
diff --git a/Makefile b/Makefile
index 08bd8b3..7399eb3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,41 +1,27 @@
 
-TEST_TARGETS =
-ITEST_TARGETS = -m integration
-UTEST_TARGETS = -m "not(integration)"
+REBUILD_FLAG =
 
-DEBUG=
+.PHONY: all
+all: venv test
 
-all: _tests
+.PHONY: venv
+venv: .venv.touch
+	tox -e venv $(REBUILD_FLAG)
 
-integration:
-	$(eval TEST_TARGETS := $(ITEST_TARGETS))
-
-unit:
-	$(eval TEST_TARGETS := $(UTEST_TARGETS))
-
-utests: test
-utest: test
+.PHONY: tests test
 tests: test
-test: unit _tests
-itests: itest
-itest: integration _tests
+test: .venv.touch
+	tox $(REBUILD_FLAG)
 
-_tests: py_env
-	bash -c 'source py_env/bin/activate && py.test tests $(TEST_TARGETS) $(DEBUG)'
 
-ucoverage: unit coverage
-icoverage: integration coverage
+.venv.touch: setup.py requirements.txt requirements_dev.txt
+	$(eval REBUILD_FLAG := --recreate)
+	touch .venv.touch
 
-coverage: py_env
-	bash -c 'source py_env/bin/activate && \
-		coverage erase && \
-		coverage run `which py.test` tests $(TEST_TARGETS) && \
-		coverage report -m'
 
-py_env: requirements.txt setup.py
-	rm -rf py_env
-	virtualenv py_env
-	bash -c 'source py_env/bin/activate && pip install -r requirements.txt'
-
+.PHONY: clean
 clean:
-	rm -rf py_env
+	find . -iname '*.pyc' | xargs rm -f
+	rm -rf .tox
+	rm -rf ./venv-*
+	rm -f .venv.touch
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
index 0000000..68a49da
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+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 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.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/pre_commit_hooks/check_yaml.py b/pre_commit_hooks/check_yaml.py
index c297ccf..456f1f7 100644
--- a/pre_commit_hooks/check_yaml.py
+++ b/pre_commit_hooks/check_yaml.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 
 import argparse
 import sys
@@ -17,7 +18,7 @@
         try:
             yaml.load(open(filename))
         except yaml.YAMLError as e:
-            print e
+            print(e)
             retval = 1
     return retval
 
diff --git a/pre_commit_hooks/debug_statement_hook.py b/pre_commit_hooks/debug_statement_hook.py
index 565740e..92f7c07 100644
--- a/pre_commit_hooks/debug_statement_hook.py
+++ b/pre_commit_hooks/debug_statement_hook.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 
 import argparse
 import ast
@@ -39,7 +40,14 @@
     visitor.visit(ast_obj)
     if visitor.debug_import_statements:
         for debug_statement in visitor.debug_import_statements:
-            print '{0}:{2}:{3} - {1} imported'.format(filename, *debug_statement)
+            print(
+                '{0}:{1}:{2} - {3} imported'.format(
+                    filename,
+                    debug_statement.line,
+                    debug_statement.col,
+                    debug_statement.name,
+                )
+            )
         return 1
     else:
         return 0
diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py
index b585ce3..9fee9c1 100644
--- a/pre_commit_hooks/end_of_file_fixer.py
+++ b/pre_commit_hooks/end_of_file_fixer.py
@@ -1,4 +1,3 @@
-
 from __future__ import print_function
 from __future__ import unicode_literals
 
@@ -18,11 +17,11 @@
         return 0
     last_character = file_obj.read(1)
     # last_character will be '' for an empty file
-    if last_character != '\n' and last_character != '':
-        file_obj.write('\n')
+    if last_character != b'\n' and last_character != b'':
+        file_obj.write(b'\n')
         return 1
 
-    while last_character == '\n':
+    while last_character == b'\n':
         # Deal with the beginning of the file
         if file_obj.tell() == 1:
             # If we've reached the beginning of the file and it is all
diff --git a/pre_commit_hooks/tests_should_end_in_test.py b/pre_commit_hooks/tests_should_end_in_test.py
index 37db03c..92641f0 100644
--- a/pre_commit_hooks/tests_should_end_in_test.py
+++ b/pre_commit_hooks/tests_should_end_in_test.py
@@ -1,4 +1,3 @@
-
 from __future__ import print_function
 
 import sys
@@ -22,4 +21,4 @@
 
 
 if __name__ == '__main__':
-    sys.exit(entry())
+    sys.exit(validate_files())
diff --git a/pre_commit_hooks/trailing_whitespace_fixer.py b/pre_commit_hooks/trailing_whitespace_fixer.py
index bf016af..20b08fe 100644
--- a/pre_commit_hooks/trailing_whitespace_fixer.py
+++ b/pre_commit_hooks/trailing_whitespace_fixer.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 
 import argparse
 import sys
@@ -12,13 +13,13 @@
     parser.add_argument('filenames', nargs='*', help='Filenames to fix')
     args = parser.parse_args(argv)
 
-    bad_whitespace_files = filter(bool, local['grep'][
+    bad_whitespace_files = local['grep'][
         ('-l', '[[:space:]]$') + tuple(args.filenames)
-    ](retcode=None).splitlines())
+    ](retcode=None).strip().splitlines()
 
     if bad_whitespace_files:
         for bad_whitespace_file in bad_whitespace_files:
-            print 'Fixing {0}'.format(bad_whitespace_file)
+            print('Fixing {0}'.format(bad_whitespace_file))
             local['sed']['-i', '-e', 's/[[:space:]]*$//', bad_whitespace_file]()
         return 1
     else:
diff --git a/pre_commit_hooks/util.py b/pre_commit_hooks/util.py
index 22d13c4..5b65794 100644
--- a/pre_commit_hooks/util.py
+++ b/pre_commit_hooks/util.py
@@ -1,4 +1,3 @@
-
 import functools
 import sys
 
diff --git a/pylintrc b/pylintrc
new file mode 100644
index 0000000..9895d8b
--- /dev/null
+++ b/pylintrc
@@ -0,0 +1,19 @@
+[MESSAGES CONTROL]
+disable=missing-docstring,abstract-method,redefined-builtin,invalid-name,no-value-for-parameter,redefined-outer-name,no-member,bad-open-mode
+
+[REPORTS]
+output-format=colorized
+reports=no
+
+[BASIC]
+const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$
+
+[FORMAT]
+max-line-length=131
+
+[TYPECHECK]
+ignored-classes=pytest
+
+[DESIGN]
+min-public-methods=0
+
diff --git a/requirements.txt b/requirements.txt
index b63c160..9c558e3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1 @@
--e .
-
-# Testing requirements
-coverage
-flake8
-mock
-git+git://github.com/pre-commit/pre-commit#egg=pre-commit
-pytest
+.
diff --git a/requirements_dev.txt b/requirements_dev.txt
new file mode 100644
index 0000000..9051674
--- /dev/null
+++ b/requirements_dev.txt
@@ -0,0 +1,8 @@
+-e .
+
+coverage
+flake8
+mock
+pylint
+pytest
+git+git://github.com/pre-commit/pre-commit#egg=pre_commit
diff --git a/setup.py b/setup.py
index 094a989..5454b48 100644
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,25 @@
 from setuptools import find_packages
 from setuptools import setup
 
+
 setup(
     name='pre_commit_hooks',
+    description='Some out-of-the-box hooks for pre-commit.',
+    url='https://github.com/pre-commit/pre-commit-hooks',
     version='0.0.0',
+
+    author='Anthony Sottile',
+    author_email='asottile@umich.edu',
+
+    platforms='linux',
+    classifiers=[
+        'License :: Public Domain',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: Implementation :: PyPy',
+    ],
+
     packages=find_packages('.', exclude=('tests*', 'testing*')),
     install_requires=[
         'argparse',
diff --git a/testing/util.py b/testing/util.py
index c52a8cf..8e468d6 100644
--- a/testing/util.py
+++ b/testing/util.py
@@ -1,4 +1,3 @@
-
 import os.path
 
 
diff --git a/tests/check_yaml_test.py b/tests/check_yaml_test.py
index 8985e8c..c145fdc 100644
--- a/tests/check_yaml_test.py
+++ b/tests/check_yaml_test.py
@@ -1,4 +1,3 @@
-
 import pytest
 
 from pre_commit_hooks.check_yaml import check_yaml
diff --git a/tests/conftest.py b/tests/conftest.py
deleted file mode 100644
index 762b31d..0000000
--- a/tests/conftest.py
+++ /dev/null
@@ -1,11 +0,0 @@
-
-import __builtin__
-
-import mock
-import pytest
-
-
-@pytest.yield_fixture
-def print_mock():
-    with mock.patch.object(__builtin__, 'print', autospec=True) as mock_print:
-        yield mock_print
diff --git a/tests/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py
index 01ecdc1..a054426 100644
--- a/tests/debug_statement_hook_test.py
+++ b/tests/debug_statement_hook_test.py
@@ -1,4 +1,3 @@
-
 import ast
 import pytest
 
diff --git a/tests/end_of_file_fixer_test.py b/tests/end_of_file_fixer_test.py
index 444836e..2e53246 100644
--- a/tests/end_of_file_fixer_test.py
+++ b/tests/end_of_file_fixer_test.py
@@ -1,5 +1,4 @@
-
-import cStringIO
+import io
 import os.path
 import pytest
 
@@ -9,19 +8,19 @@
 
 # Input, expected return value, expected output
 TESTS = (
-    ('foo\n', 0, 'foo\n'),
-    ('', 0, ''),
-    ('\n\n', 1, ''),
-    ('\n\n\n\n', 1, ''),
-    ('foo', 1, 'foo\n'),
-    ('foo\n\n\n', 1, 'foo\n'),
-    ('\xe2\x98\x83', 1, '\xe2\x98\x83\n'),
+    (b'foo\n', 0, b'foo\n'),
+    (b'', 0, b''),
+    (b'\n\n', 1, b''),
+    (b'\n\n\n\n', 1, b''),
+    (b'foo', 1, b'foo\n'),
+    (b'foo\n\n\n', 1, b'foo\n'),
+    (b'\xe2\x98\x83', 1, b'\xe2\x98\x83\n'),
 )
 
 
 @pytest.mark.parametrize(('input', 'expected_retval', 'output'), TESTS)
 def test_fix_file(input, expected_retval, output):
-    file_obj = cStringIO.StringIO()
+    file_obj = io.BytesIO()
     file_obj.write(input)
     ret = fix_file(file_obj)
     assert file_obj.getvalue() == output
@@ -32,11 +31,11 @@
 def test_integration(input, expected_retval, output, tmpdir):
     file_path = os.path.join(tmpdir.strpath, 'file.txt')
 
-    with open(file_path, 'w') as file_obj:
+    with open(file_path, 'wb') as file_obj:
         file_obj.write(input)
 
     ret = end_of_file_fixer([file_path])
-    file_output = open(file_path, 'r').read()
+    file_output = open(file_path, 'rb').read()
 
     assert file_output == output
     assert ret == expected_retval
diff --git a/tests/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py
index e56b84e..3ba1617 100644
--- a/tests/tests_should_end_in_test_test.py
+++ b/tests/tests_should_end_in_test_test.py
@@ -1,14 +1,11 @@
-
 from pre_commit_hooks.tests_should_end_in_test import validate_files
 
 
-def test_validate_files_all_pass(print_mock):
+def test_validate_files_all_pass():
     ret = validate_files(['foo_test.py', 'bar_test.py'])
     assert ret == 0
-    assert print_mock.call_count == 0
 
 
-def test_validate_files_one_fails(print_mock):
+def test_validate_files_one_fails():
     ret = validate_files(['not_test_ending.py', 'foo_test.py'])
     assert ret == 1
-    assert print_mock.call_count == 1
diff --git a/tests/util_test.py b/tests/util_test.py
index 84fa6c5..28ebe81 100644
--- a/tests/util_test.py
+++ b/tests/util_test.py
@@ -1,4 +1,3 @@
-
 import mock
 import pytest
 import sys
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..7dccc5a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,28 @@
+[tox]
+project = pre_commit_hooks
+# These should match the travis env list
+envlist = py26,py27,py33,pypy
+
+[testenv]
+install_command = pip install --use-wheel {opts} {packages}
+deps = -rrequirements_dev.txt
+commands =
+    coverage erase
+    coverage run -m pytest {posargs:tests}
+    coverage report --show-missing --fail-under 82
+    flake8 {[tox]project} testing tests setup.py
+    pylint {[tox]project} testing tests setup.py
+
+[testenv:venv]
+envdir = venv-{[tox]project}
+commands =
+
+[testenv:docs]
+deps =
+    {[testenv]deps}
+    sphinx
+changedir = docs
+commands = sphinx-build -b html -d build/doctrees source build/html
+
+[flake8]
+max-line-length=131