Repository: frictionlessdata/tabulator-py (python)

Issue:
The way to get list of dicts from table?
Core interface provide emitting `Row` instances (`tuple` subclass, with `headers, values, get()`) from `Table`. 

But it looks like some clients in need to get fully functional `dict` in item emitting.

Pull request description:
Feature/list of dicts
It fixes #39

Patch:
diff --git a/README.md b/README.md
index d695a5a..24eef23 100644
--- a/README.md
+++ b/README.md
@@ -76,8 +76,9 @@ table = topen('path.csv',
         parser_options={'delimeter': ',', quotechar: '|'},
         loader_class=loaders.File,
         parser_class=parsers.CSV,
+        table_class=CustomTable,
         iterator_class=CustomIterator,
-        table_class=CustomTable)
+        row_class=CustomRow)
 table.add_processor(processors.Headers(skip=1))
 headers = table.headers
 contents = table.read(limit=10)
@@ -85,10 +86,6 @@ print(headers, contents)
 table.close()
 ```
 
-Also `Table` class can be instantiated by user (see documentation).
-But there is no difference between it and `topen` call with extended
-list of parameters except `topen` also calls the `table.open()` method.
-
 ## Design Overview
 
 Tabulator uses modular architecture to be fully extensible and flexible.
diff --git a/examples/topen.py b/examples/topen.py
index 4a36ea8..3835eea 100644
--- a/examples/topen.py
+++ b/examples/topen.py
@@ -98,6 +98,14 @@ with topen(source, with_headers=True) as table:
         print(row)
 
 
+print('\nKeyed rows with row_class=dict:')
+source = [{'id': '1', 'name': 'english'}, {'id': '2', 'name': '中国人'}]
+with topen(source, with_headers=True, row_class=dict) as table:
+    print(table.headers)
+    for row in table:
+        print(row)
+
+
 print('\nTable reset and read limit:')
 source = 'data/table.csv'
 with topen(source, with_headers=True) as table:
diff --git a/tabulator/table.py b/tabulator/table.py
index 8b6d5ff..f26cf14 100644
--- a/tabulator/table.py
+++ b/tabulator/table.py
@@ -27,14 +27,17 @@ class Table(object):
 
     # Public
 
-    def __init__(self, loader, parser, iterator_class=None):
+    def __init__(self, loader, parser, iterator_class=None, row_class=None):
 
         # Default values
         if iterator_class is None:
             iterator_class = Iterator
+        if row_class is None:
+            row_class = Row
 
         # Set attributes
         self.__iterator_class = iterator_class
+        self.__row_class = row_class
         self.__loader = loader
         self.__parser = parser
         self.__processors = []
@@ -62,7 +65,15 @@ class Table(object):
 
         # Get the next row
         self.__iterator.__next__()
-        row = Row(self.__iterator.headers, self.__iterator.values)
+        if issubclass(self.__row_class, Row):
+            row = self.__row_class(
+                self.__iterator.headers, self.__iterator.values)
+        elif issubclass(self.__row_class, dict):
+            if self.__iterator.headers is None:
+                raise ValueError('Headers are required for dict row class')
+            row = dict(zip(self.__iterator.headers, self.__iterator.values))
+        else:
+            raise TypeError('Row class should be tabulator.Row or dict')
 
         return row
 
diff --git a/tabulator/topen.py b/tabulator/topen.py
index 3e37834..b1f2ced 100644
--- a/tabulator/topen.py
+++ b/tabulator/topen.py
@@ -4,6 +4,7 @@ from __future__ import print_function
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
+from .row import Row
 from .table import Table
 from .iterator import Iterator
 from .processors import Headers
@@ -20,8 +21,7 @@ def topen(source,
           scheme=None, format=None, encoding=None,
           loader_options=None, parser_options=None,
           loader_class=None, parser_class=None,
-          iterator_class=None,
-          table_class=None):
+          table_class=None, iterator_class=None, row_class=None):
     """Open table from source with scheme, encoding and format.
 
     Function `topen` is a wrapper around `Table` interface.
@@ -66,10 +66,12 @@ def topen(source,
         Loader class.
     parser_class: type
         Parser class.
-    iterator_class: type
-        Iterator class.
     table_class: type
         Table class.
+    iterator_class: type
+        Iterator class.
+    row_class: type
+        Row class.
 
     Returns
     -------
@@ -82,10 +84,12 @@ def topen(source,
         loader_options = {}
     if parser_options is None:
         parser_options = {}
-    if iterator_class is None:
-        iterator_class = Iterator
     if table_class is None:
         table_class = Table
+    if iterator_class is None:
+        iterator_class = Iterator
+    if row_class is None:
+        row_class = Row
 
     # Get loader
     if loader_class is None:
@@ -111,7 +115,8 @@ def topen(source,
     table = table_class(
             loader=loader,
             parser=parser,
-            iterator_class=iterator_class)
+            iterator_class=iterator_class,
+            row_class=row_class)
     table.open()
 
     # Add headers processor


Test patch:
diff --git a/tests/test_topen.py b/tests/test_topen.py
index 1b55bf8..63f7a31 100644
--- a/tests/test_topen.py
+++ b/tests/test_topen.py
@@ -248,6 +248,18 @@ class Test_topen(unittest.TestCase):
         assert table.headers == ('id', 'name')
         assert table.read() == [('1', 'english'), ('2', '中国人')]
 
+    def test_headers_row_class_dict(self):
+
+        # Get table
+        source = [['id', 'name'], ['1', 'english'], ('2', '中国人')]
+        table = topen(source, with_headers=True, row_class=dict)
+
+        # Make assertions
+        assert table.headers == ('id', 'name')
+        assert table.read() == [
+            {'id': '1', 'name': 'english'},
+            {'id': '2', 'name': '中国人'}]
+
     # It works for Python 2 but values convertion differs
     @pytest.mark.skipif(sys.version_info < (3,3), reason='requires python 3.3')
     def test_convert(self):