Repository: blaze/blaze

Issue:
ReLabel expression repr should print a dictionary if any names are not valid Python identifiers
xref: https://github.com/ContinuumIO/blaze/pull/758#issuecomment-97871354



Discussion:

Resolving patch:
diff --git a/blaze/expr/expressions.py b/blaze/expr/expressions.py
--- a/blaze/expr/expressions.py
+++ b/blaze/expr/expressions.py
@@ -2,13 +2,14 @@
 
 import toolz
 import datashape
-import functools
-import keyword
+import re
+
+from keyword import iskeyword
+
 import numpy as np
 
-from toolz import concat, memoize, partial
+from toolz import concat, memoize, partial, first
 from toolz.curried import map, filter
-import re
 
 from datashape import dshape, DataShape, Record, Var, Mono, Fixed
 from datashape.predicates import isscalar, iscollection, isboolean, isrecord
@@ -27,7 +28,7 @@
 _attr_cache = dict()
 
 
-def isvalid_identifier(s, regex=re.compile('^[_a-zA-Z][_a-zA-Z0-9]*$')):
+def isvalid_identifier(s):
     """Check whether a string is a valid Python identifier
 
     Examples
@@ -47,7 +48,9 @@ def isvalid_identifier(s, regex=re.compile('^[_a-zA-Z][_a-zA-Z0-9]*$')):
     >>> isvalid_identifier(None)
     False
     """
-    return not not s and not keyword.iskeyword(s) and regex.match(s) is not None
+    # the re module compiles and caches regexs so no need to compile it
+    return (s is not None and not iskeyword(s) and
+            re.match(r'^[_a-zA-Z][_a-zA-Z0-9]*$', s) is not None)
 
 
 def valid_identifier(s):
@@ -533,6 +536,23 @@ class ReLabel(ElemWise):
     Traceback (most recent call last):
         ...
     ValueError: Cannot relabel non-existent child fields: {'not_a_column'}
+    >>> s = symbol('s', 'var * {"0": int64}')
+    >>> s.relabel({'0': 'foo'})
+    s.relabel({'0': 'foo'})
+    >>> s.relabel(0='foo') # doctest: +SKIP
+    Traceback (most recent call last):
+        ...
+    SyntaxError: keyword can't be an expression
+
+    Notes
+    -----
+    When names are not valid Python names, such as integers, you must pass a
+    dictionary to ``relabel``. For example
+
+    .. code-block:: python
+
+       s = symbol('s', 'var * {"0": int64}')
+       s.relabel({'0': 'foo'})
 
     See Also
     --------
@@ -548,8 +568,12 @@ def schema(self):
                                  for name, dtype in param]))
 
     def __str__(self):
-        return ('%s.relabel(%s)' %
-                (self._child, ', '.join('%s=%r' % l for l in self.labels)))
+        labels = self.labels
+        if all(map(isvalid_identifier, map(first, labels))):
+            rest = ', '.join('%s=%r' % l for l in labels)
+        else:
+            rest = '{%s}' % ', '.join('%r: %r' % l for l in labels)
+        return '%s.relabel(%s)' % (self._child, rest)
 
 
 def relabel(child, labels=None, **kwargs):


Test patch:
diff --git a/blaze/expr/tests/test_expr.py b/blaze/expr/tests/test_expr.py
--- a/blaze/expr/tests/test_expr.py
+++ b/blaze/expr/tests/test_expr.py
@@ -1,9 +1,9 @@
 from __future__ import absolute_import, division, print_function
+
+import pytest
 from datashape import dshape
 
-from blaze.expr import *
-from blaze.expr.core import subs
-from blaze.utils import raises
+from blaze.expr import symbol, label, Field
 
 
 def test_Symbol():
@@ -12,12 +12,15 @@ def test_Symbol():
     assert e.shape == (3, 5)
     assert str(e) == 'e'
 
+
 def test_symbol_caches():
     assert symbol('e', 'int') is symbol('e', 'int')
 
+
 def test_Symbol_tokens():
-    assert symbol('x', 'int').isidentical(Symbol('x', 'int'))
-    assert not symbol('x', 'int').isidentical(Symbol('x', 'int', 1))
+    assert symbol('x', 'int').isidentical(symbol('x', 'int'))
+    assert not symbol('x', 'int').isidentical(symbol('x', 'int', 1))
+
 
 def test_Field():
     e = symbol('e', '3 * 5 * {name: string, amount: int}')
@@ -28,12 +31,15 @@ def test_Field():
 
 
 def test_nested_fields():
-    e = symbol('e', '3 * {name: string, payments: var * {amount: int, when: datetime}}')
-    assert e.payments.dshape == dshape('3 * var * {amount: int, when: datetime}')
+    e = symbol(
+        'e', '3 * {name: string, payments: var * {amount: int, when: datetime}}')
+    assert e.payments.dshape == dshape(
+        '3 * var * {amount: int, when: datetime}')
     assert e.payments.schema == dshape('{amount: int, when: datetime}')
     assert 'amount' in dir(e.payments)
     assert e.payments.amount.dshape == dshape('3 * var * int')
 
+
 def test_partialed_methods_have_docstrings():
     e = symbol('e', '3 * 5 * {name: string, amount: int}')
     assert 'string comparison' in e.like.__doc__
@@ -43,11 +49,17 @@ def test_relabel():
     e = symbol('e', '{name: string, amount: int}')
     assert e.relabel(amount='balance').fields == ['name', 'balance']
 
+
 def test_meaningless_relabel_doesnt_change_input():
     e = symbol('e', '{name: string, amount: int}')
     assert e.relabel(amount='amount').isidentical(e)
 
 
+def test_relabel_with_invalid_identifiers_reprs_as_dict():
+    s = symbol('s', '{"0": int64}')
+    assert repr(s.relabel({'0': 'foo'})) == "s.relabel({'0': 'foo'})"
+
+
 def test_dir():
     e = symbol('e', '3 * 5 * {name: string, amount: int, x: real}')
 
@@ -99,13 +111,13 @@ def test_symbol_subs():
 
 def test_multiple_renames_on_series_fails():
     t = symbol('s', 'var * {timestamp: datetime}')
-    ts = t.timestamp
-    assert raises(ValueError, lambda: ts.relabel({'timestamp': 'date',
-                                                  'hello': 'world'}))
+    with pytest.raises(ValueError):
+        t.timestamp.relabel(timestamp='date', hello='world')
 
 
 def test_map_with_rename():
     t = symbol('s', 'var * {timestamp: datetime}')
     result = t.timestamp.map(lambda x: x.date(), schema='{date: datetime}')
-    assert raises(ValueError, lambda: result.relabel({'timestamp': 'date'}))
+    with pytest.raises(ValueError):
+        result.relabel(timestamp='date')
     assert result.fields == ['date']