Repository: ibis-project/ibis

Issue:
BUG: Expression formatting not working for nested lists
```
import ibis
lit = ibis.literal([[1,2],[3,4]])
lit
```
gives `TypeError: unhashable type: 'list'`


Discussion:

Resolving patch:
diff --git a/ibis/expr/datatypes.py b/ibis/expr/datatypes.py
--- a/ibis/expr/datatypes.py
+++ b/ibis/expr/datatypes.py
@@ -25,6 +25,7 @@
 
 import ibis.common as com
 import ibis.expr.types as ir
+from ibis import util
 
 
 class DataType:
@@ -112,6 +113,10 @@ def scalar_type(self):
     def column_type(self):
         return functools.partial(self.column, dtype=self)
 
+    def _literal_value_hash_key(self, value) -> int:
+        """Return a hash for `value`."""
+        return self, value
+
 
 class Any(DataType):
     __slots__ = ()
@@ -516,6 +521,22 @@ def __str__(self) -> str:
             ', '.join(itertools.starmap('{}: {}'.format, self.pairs.items())),
         )
 
+    def _literal_value_hash_key(self, value):
+        return self, _tuplize(value.items())
+
+
+def _tuplize(values):
+    """Recursively convert `values` to a tuple of tuples."""
+    def tuplize_iter(values):
+        yield from (
+            tuple(tuplize_iter(value))
+            if util.is_iterable(value)
+            else value
+            for value in values
+        )
+
+    return tuple(tuplize_iter(values))
+
 
 class Array(Variadic):
     scalar = ir.ArrayScalar
@@ -532,6 +553,9 @@ def __init__(
     def __str__(self) -> str:
         return '{}<{}>'.format(self.name.lower(), self.value_type)
 
+    def _literal_value_hash_key(self, value):
+        return self, _tuplize(value)
+
 
 class Set(Variadic):
     scalar = ir.SetScalar
@@ -581,6 +605,9 @@ def __str__(self) -> str:
             self.name.lower(), self.key_type, self.value_type
         )
 
+    def _literal_value_hash_key(self, value):
+        return self, _tuplize(value.items())
+
 
 class GeoSpatial(DataType):
     __slots__ = 'geotype', 'srid'
diff --git a/ibis/expr/format.py b/ibis/expr/format.py
--- a/ibis/expr/format.py
+++ b/ibis/expr/format.py
@@ -166,17 +166,14 @@ def _indent(self, text, indents=1):
     def _format_table(self, expr):
         table = expr.op()
         # format the schema
-        rows = ['name: {0!s}\nschema:'.format(table.name)]
+        rows = ['name: {}\nschema:'.format(table.name)]
         rows.extend(
-            [
-                '  %s : %s' % tup
-                for tup in zip(table.schema.names, table.schema.types)
-            ]
+            map('  {} : {}'.format, table.schema.names, table.schema.types)
         )
         opname = type(table).__name__
         type_display = self._get_type_display(expr)
-        opline = '%s[%s]' % (opname, type_display)
-        return '{0}\n{1}'.format(opline, self._indent('\n'.join(rows)))
+        opline = '{}[{}]'.format(opname, type_display)
+        return '{}\n{}'.format(opline, self._indent('\n'.join(rows)))
 
     def _format_column(self, expr):
         # HACK: if column is pulled from a Filter of another table, this parent
diff --git a/ibis/expr/operations.py b/ibis/expr/operations.py
--- a/ibis/expr/operations.py
+++ b/ibis/expr/operations.py
@@ -2901,6 +2901,15 @@ def output_type(self):
     def root_tables(self):
         return []
 
+    def __hash__(self) -> int:
+        """Return the hash of a literal value.
+
+        We override this method to make sure that we can handle things that
+        aren't eminently hashable like an ``array<array<int64>>``.
+
+        """
+        return hash(self.dtype._literal_value_hash_key(self.value))
+
 
 class NullLiteral(Literal):
     """Typeless NULL literal"""


Test patch:
diff --git a/ibis/expr/tests/test_value_exprs.py b/ibis/expr/tests/test_value_exprs.py
--- a/ibis/expr/tests/test_value_exprs.py
+++ b/ibis/expr/tests/test_value_exprs.py
@@ -1540,3 +1540,34 @@ def test_chained_select_on_join():
     expr1 = join["a", "b"]
     expr2 = join.select(["a", "b"])
     assert expr1.equals(expr2)
+
+
+def test_repr_list_of_lists():
+    lit = ibis.literal([[1]])
+    result = repr(lit)
+    expected = """\
+Literal[array<array<int8>>]
+  [[1]]"""
+    assert result == expected
+
+
+def test_repr_list_of_lists_in_table():
+    t = ibis.table([('a', 'int64')], name='t')
+    lit = ibis.literal([[1]])
+    expr = t[t, lit.name('array_of_array')]
+    result = repr(expr)
+    expected = """\
+ref_0
+UnboundTable[table]
+  name: t
+  schema:
+    a : int64
+
+Selection[table]
+  table:
+    Table: ref_0
+  selections:
+    Table: ref_0
+    array_of_array = Literal[array<array<int8>>]
+      [[1]]"""
+    assert result == expected