Repository: 42DIGITAL/bqtools

Issue:
append data to empty table.data fails - required for schema_only
```python
>>> from fourtytwo import bqtools
>>> table = bqtools.read_bq(table_ref='project.dataset.table', schema_only=True)
>>> table.append([])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/bqtools/fourtytwo/bqtools/__init__.py", line 224, in append
    for index in range(len(data)):
TypeError: object of type 'NoneType' has no len()
```

File "/bqtools/fourtytwo/bqtools/\_\_init\_\_.py", line 224, in append:
```python
    def append(self, rows):
        append_columns = _rows_to_columns(rows, self.schema)
        data = self.data
        for index in range(len(data)):
            data[index] += append_columns[index]
```

Probably replace range(len(data)) with range(len(self.schema))

Discussion:

Patch:
diff --git a/fourtytwo/bqtools/__init__.py b/fourtytwo/bqtools/__init__.py
index 9542242..136e18c 100644
--- a/fourtytwo/bqtools/__init__.py
+++ b/fourtytwo/bqtools/__init__.py
@@ -104,14 +104,21 @@ class BQTable(object):
     def __init__(self, schema=None, data=None):
         if DEBUG:
             logging.debug('bqtools.BQTable.__init__')
-
+        
         self.schema = schema if schema else []
         self.data = data if data else []
     
-    #def __repr__(self):
-    #    pass
+    def __repr__(self):
+        schema_shape = len(self.schema)
+        if len(self.data) > 0:
+            data_shape = (len(self.data[0]), len(self.data))
+        else:
+            data_shape = (0,)
+        return '<bqtools.BQTable(shape_schema={}, shape_data={})>'.format(schema_shape, data_shape)
 
     def __eq__(self, other):
+        if not isinstance(other, BQTable):
+            raise TypeError('other must be of type BQTable')
         return self.schema == other.schema and self.data == other.data
     
     def __setattr__(self, name, value):
@@ -146,13 +153,18 @@ class BQTable(object):
                 new_schema.append(bigquery.SchemaField(**field))
         
         if self.schema and new_schema and new_schema != self.schema:
-            data = self._move_columns(new_schema)
+            # TODO Handle appends to schema
+            # if len(new_schema) > len(self.schema)
+            data = self._move_columns(schema=new_schema)
         else:
             data = self.data
-
-        data = self._typecheck(schema=new_schema, data=data)
-        object.__setattr__(self, '_schema', new_schema)
-        object.__setattr__(self, '_data', data)
+        
+        if data:
+            data = self._typecheck(schema=new_schema, data=data)
+            object.__setattr__(self, '_schema', new_schema)
+            object.__setattr__(self, '_data', data)
+        else:
+            object.__setattr__(self, '_schema', new_schema)
 
     def _set_data(self, data):
         if DEBUG:
@@ -160,14 +172,14 @@ class BQTable(object):
         
         if data and isinstance(data, list):
             if isinstance(data[0], dict):
-                data = _rows_to_columns(data, self.schema)
-        data = self._typecheck(data=data)
+                data = _rows_to_columns(rows=data, schema=self.schema)
+            data = self._typecheck(data=data)
         object.__setattr__(self, '_data', data)
     
     def _move_columns(self, schema):
         if DEBUG:
             logging.debug('bqtools.BQTable._move_columns()')
-
+        
         old_field_names = [field.name for field in self.schema]
         new_field_names = [field.name for field in schema]
         column_order = [old_field_names.index(name) for name in new_field_names]
@@ -219,8 +231,8 @@ class BQTable(object):
         self._rename_columns(mapping=columns)
     
     def append(self, rows):
-        append_columns = _rows_to_columns(rows, self.schema)
-        data = self.data
+        append_columns = _rows_to_columns(rows=rows, schema=self.schema)
+        data = self.data if self.data else [[] for n in range(len(self.schema))]
         for index in range(len(data)):
             data[index] += append_columns[index]
         self.data = data


Test patch:
diff --git a/tests/test_bqtools.py b/tests/test_bqtools.py
index 0202f79..f990c0c 100644
--- a/tests/test_bqtools.py
+++ b/tests/test_bqtools.py
@@ -1,6 +1,6 @@
 from fourtytwo import bqtools
 
-def test_bqorm_construct_columns():
+def test_bqtools_construct_columns():
     schema = [
         {'name': 'number', 'field_type': 'INTEGER'},
         {'name': 'text', 'field_type': 'STRING'},
@@ -12,7 +12,7 @@ def test_bqorm_construct_columns():
     assert len(table.data) == 2
     assert len(table.rows()) == 4
 
-def test_bqorm_construct_rows_dicts():
+def test_bqtools_construct_rows_dicts():
     schema = [
         {'name': 'number', 'field_type': 'INTEGER'},
         {'name': 'text', 'field_type': 'STRING'},
@@ -28,3 +28,46 @@ def test_bqorm_construct_rows_dicts():
     assert len(table.schema) == 2
     assert len(table.data) == 2
     assert len(table.rows()) == 4
+
+def test_bqtools_append_list():
+    schema = [
+        {'name': 'number', 'field_type': 'INTEGER'},
+        {'name': 'text', 'field_type': 'STRING'},
+    ]
+    table = bqtools.BQTable(schema=schema)
+    table.append([[1, 'a'], [2, 'b']])
+    assert len(table.data) == 2
+    assert len(table.rows()) == 2
+
+def test_bqtools_append_dict():
+    schema = [
+        {'name': 'number', 'field_type': 'INTEGER'},
+        {'name': 'text', 'field_type': 'STRING'},
+    ]
+    table = bqtools.BQTable(schema=schema)
+    table.append([
+        {'number': 1, 'text': 'a'},
+        {'number': 2, 'text': 'b'}
+    ])
+    assert len(table.data) == 2
+    assert len(table.rows()) == 2
+
+def test_bqtools_append_schema():
+    schema = [
+        {'name': 'number', 'field_type': 'INTEGER'},
+        {'name': 'text', 'field_type': 'STRING'},
+    ]
+    rows = [
+        {'number': 1, 'text': 'a'},
+        {'number': 2, 'text': 'b'},
+        {'number': 3, 'text': 'c'},
+        {'number': 4, 'text': 'd'},
+    ]
+    table = bqtools.BQTable(schema=schema, data=rows)
+    
+    table.schema += [{'name': 'test', 'field_type': 'STRING'}]
+    table.schema = table.schema + [{'name': 'test', 'field_type': 'STRING'}]
+    assert len(table.schema) == 3
+    assert len(table.data) == 3
+    assert len(table.rows()) == 4
+