Fixes, Python 3 support.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c445023
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+# pytoml
+
+This project aims at being a specs-conforming and strict parser for [TOML][1] files.
+The parser currently supports [version 0.4.0][2] of the specs.
+
+The project supports Python 2.7 and 3.4+.
+
+Install:
+
+    easy_install pytoml
+
+The interface is the same as for the standard `json` package.
+
+    >>> import pytoml as toml
+    >>> toml.loads('a = 1')
+    {'a': 1}
+    >>> with open('file.toml', 'rb') as fin:
+    ...     toml.load(fin)
+    {'a': 1}
+
+The `loads` function accepts either a bytes object
+(that gets decoded as UTF-8 with no BOM allowed),
+or a unicode object.
+
+## Installation
+
+    easy_install pytoml
+
+  [1]: https://github.com/toml-lang/toml
+  [2]: https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md
diff --git a/pytoml/__init__.py b/pytoml/__init__.py
new file mode 100644
index 0000000..e20c2dd
--- /dev/null
+++ b/pytoml/__init__.py
@@ -0,0 +1 @@
+from .parser import TomlError, load, loads
diff --git a/toml.py b/pytoml/parser.py
similarity index 86%
rename from toml.py
rename to pytoml/parser.py
index bae4b3e..005aede 100644
--- a/toml.py
+++ b/pytoml/parser.py
@@ -1,22 +1,27 @@
-import string, re
+import string, re, sys
 
 class TomlError(RuntimeError):
-    def __init__(self, kind, line, col):
-        self.kind = kind
+    def __init__(self, message, line, col, filename):
+        RuntimeError.__init__(self, message, line, col, filename)
+        self.message = message
         self.line = line
         self.col = col
-        RuntimeError.__init__(self, kind)
+        self.filename = filename
 
     def __str__(self):
-        return '%s(%d, %d)' % (self.kind, self.line, self.col)
+        return '{}({}, {}): {}'.format(self.filename, self.line, self.col, self.message)
+
+    def __repr__(self):
+        return 'TomlError({!r}, {!r}, {!r}, {!r})'.format(self.message, self.line, self.col, self.filename)
 
 class _CharSource:
-    def __init__(self, s):
+    def __init__(self, s, filename):
         self._s = s
         self._index = 0
         self._mark = 0
         self._line = 1
         self._col = 1
+        self._filename = filename
         self._update_cur()
 
     def __bool__(self):
@@ -56,8 +61,8 @@
             text = tok
         return type, text, pos
 
-    def error(self, kind):
-        raise TomlError(kind, self._line, self._col)
+    def error(self, message):
+        raise TomlError(message, self._line, self._col, self._filename)
 
     def _update_cur(self):
         self.tail = self._s[self._index:]
@@ -66,8 +71,13 @@
         else:
             self.cur = None
 
-def lex(s):
-    src = _CharSource(s.replace('\r\n', '\n'))
+if sys.version_info[0] == 2:
+    _chr = unichr
+else:
+    _chr = chr
+
+def _lex(s, filename):
+    src = _CharSource(s.replace('\r\n', '\n'), filename)
     def is_id(ch):
         return ch is not None and (ch.isalnum() or ch in '-_')
 
@@ -79,12 +89,12 @@
         if src.cur == 'u':
             if len(src) < 5 or any(ch not in string.hexdigits for ch in src[1:5]):
                 src.error('invalid_escape_sequence')
-            res = unichr(int(src[1:5], 16))
+            res = _chr(int(src[1:5], 16))
             src.next(5)
         elif src.cur == 'U':
             if len(src) < 9 or any(ch not in string.hexdigits for ch in src[1:9]):
                 src.error('invalid_escape_sequence')
-            res = unichr(int(src[1:9], 16))
+            res = _chr(int(src[1:9], 16))
             src.next(9)
         elif src.cur == '\n':
             while src and src.cur in ' \n\t':
@@ -222,8 +232,9 @@
     yield src.commit('eof', '')
 
 class _TokSource:
-    def __init__(self, s):
-        self._lex = iter(lex(s))
+    def __init__(self, s, filename):
+        self._filename = filename
+        self._lex = iter(_lex(s, filename))
         self.pos = None
         self.next()
 
@@ -246,8 +257,12 @@
         while self.consume('\n'):
             pass
 
-    def error(self, kind):
-        raise TomlError(kind, self.pos[0][0], self.pos[0][1])
+    def expect(self, kind, error_text):
+        if not self.consume(kind):
+            self.error(error_text)
+
+    def error(self, message):
+        raise TomlError(message, self.pos[0][0], self.pos[0][1], self._filename)
 
 def _translate_literal(type, text):
     if type == 'bool':
@@ -262,13 +277,15 @@
         return text
 
 def load(fin, translate_literal=_translate_literal, translate_array=id):
-    return loads(fin.read(), translate_literal=translate_literal, translate_array=translate_array)
+    return loads(fin.read(),
+        translate_literal=translate_literal, translate_array=translate_array,
+        filename=fin.name)
 
-def loads(s, translate_literal=_translate_literal, translate_array=id):
-    if isinstance(s, str):
+def loads(s, translate_literal=_translate_literal, translate_array=id, filename='<string>'):
+    if isinstance(s, bytes):
         s = s.decode('utf-8')
 
-    toks = _TokSource(s)
+    toks = _TokSource(s, filename)
 
     def read_value():
         while True:
@@ -302,8 +319,7 @@
                         res.append(val)
                         toks.consume_nls()
                     else:
-                        if not toks.consume(']'):
-                            toks.error('expected_right_brace')
+                        toks.expect(']', 'expected_right_brace')
                 return 'array', translate_array(res)
             elif toks.consume('{'):
                 res = {}
@@ -312,14 +328,12 @@
                     toks.next()
                     if k in res:
                         toks.error('duplicate_key')
-                    if not toks.consume('='):
-                        toks.error('expected_equals')
+                    toks.expect('=', 'expected_equals')
                     type, v = read_value()
                     res[k] = v
                     if not toks.consume(','):
                         break
-                if not toks.consume('}'):
-                    toks.error('expected_closing_brace')
+                toks.expect('}', 'expected_closing_brace')
                 return 'table', res
             else:
                 toks.error('unexpected_token')
@@ -332,12 +346,12 @@
         if toks.tok in ('id', 'str'):
             k = toks.text
             toks.next()
-            if not toks.consume('='):
-                toks.error('expected_equals')
+            toks.expect('=', 'expected_equals')
             type, v = read_value()
             if k in scope:
                 toks.error('duplicate_keys')
             scope[k] = v
+            toks.expect('\n', 'expected_eol')
         elif toks.consume('\n'):
             pass
         elif toks.consume('['):
@@ -355,8 +369,7 @@
                 toks.next()
             if not toks.consume(']') or (is_table_array and not toks.consume_adjacent(']')):
                 toks.error('malformed_table_name')
-            if not toks.consume('\n'):
-                toks.error('garbage_after_table_name')
+            toks.expect('\n', 'expected_eol')
 
             cur = tables
             for name in path[:-1]:
@@ -389,9 +402,10 @@
     def merge_tables(scope, tables):
         if scope is None:
             scope = {}
-        for k, v in tables.iteritems():
+        for k in tables:
             if k in scope:
                 toks.error('key_table_conflict')
+            v = tables[k]
             if isinstance(v, list):
                 scope[k] = [merge_tables(sc, tbl) for sc, tbl in v]
             else:
@@ -399,8 +413,3 @@
         return scope
 
     return merge_tables(root, tables)
-
-if __name__ == '__main__':
-    import sys, json
-    t = sys.stdin.read()
-    print json.dumps(loads(t), indent=4)
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..30404df
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+from setuptools import setup
+
+setup(
+    name='pytoml',
+    version='0.1.0',
+
+    description='A parser for TOML-0.4.0',
+    author='Martin Vejnár',
+    author_email='avakar@ratatanek.cz',
+    url='https://github.com/avakar/pytoml',
+    license='MIT',
+
+    packages=['pytoml'],
+    )
diff --git a/test/invalid/text-after-table2.toml b/test/invalid/key-after-table.toml
similarity index 100%
rename from test/invalid/text-after-table2.toml
rename to test/invalid/key-after-table.toml
diff --git a/test/invalid/key-no-eol.toml b/test/invalid/key-no-eol.toml
new file mode 100644
index 0000000..3c58eee
--- /dev/null
+++ b/test/invalid/key-no-eol.toml
@@ -0,0 +1 @@
+a = 1 b = 2
diff --git a/test.py b/test/test.py
similarity index 76%
rename from test.py
rename to test/test.py
index 2b12baa..79f4756 100644
--- a/test.py
+++ b/test/test.py
@@ -1,4 +1,5 @@
-import toml, os, json, sys
+import os, json, sys, io
+import pytoml as toml
 
 def _testbench_literal(type, text):
     _type_table = {'str': 'string', 'int': 'integer'}
@@ -11,7 +12,7 @@
     succeeded = []
     failed = []
 
-    for top, dirnames, fnames in os.walk('test'):
+    for top, dirnames, fnames in os.walk('.'):
         for fname in fnames:
             if not fname.endswith('.toml'):
                 continue
@@ -23,7 +24,7 @@
                 parsed = None
 
             try:
-                with open(os.path.join(top, fname[:-5] + '.json'), 'rb') as fin:
+                with io.open(os.path.join(top, fname[:-5] + '.json'), 'rt', encoding='utf-8') as fin:
                     bench = json.load(fin)
             except IOError:
                 bench = None
@@ -34,8 +35,8 @@
                 succeeded.append(fname)
 
     for f in failed:
-        print 'failed: {f}'.format(f=f)
-    print 'succeeded: {succ}'.format(succ=len(succeeded))
+        print('failed: {}'.format(f))
+    print('succeeded: {}'.format(len(succeeded)))
     return 1 if failed else 0
 
 if __name__ == '__main__':