Repository: grabbles/grabbit

Issue:
Handle entity names that conflict with reserved keywords
In cases where an `Entity` uses a reserved keyword as its name (e.g., `class`), exceptions can occur for some `.get()` queries (e.g., when `return_type='tuple'`, because reserved keywords can't be attributes). We need to find some workaround for this--e.g., setting a different name internally, or adopting a convention of appending underscores, etc. See INCF/pybids#142 for relevant discussion.

Discussion:

Patch:
diff --git a/grabbit/core.py b/grabbit/core.py
index cdb0663..8c3c375 100644
--- a/grabbit/core.py
+++ b/grabbit/core.py
@@ -9,6 +9,7 @@ from os.path import (join, basename, dirname, abspath, split, isabs, exists)
 from functools import partial
 from copy import deepcopy
 import warnings
+from keyword import iskeyword
 
 
 __all__ = ['File', 'Entity', 'Layout']
@@ -90,7 +91,18 @@ class File(object):
         Returns the File as a named tuple. The full path plus all entity
         key/value pairs are returned as attributes.
         """
-        entities = self.entities
+        keys = list(self.entities.keys())
+        replaced = []
+        for i, k in enumerate(keys):
+            if iskeyword(k):
+                replaced.append(k)
+                keys[i] = '%s_' % k
+        if replaced:
+            safe = ['%s_' % k for k in replaced]
+            warnings.warn("Entity names cannot be reserved keywords when "
+                          "representing a File as a namedtuple. Replacing "
+                          "entities %s with safe versions %s." % (keys, safe))
+        entities = dict(zip(keys, self.entities.values()))
         _File = namedtuple('File', 'filename ' + ' '.join(entities.keys()))
         return _File(filename=self.path, **entities)
 


Test patch:
diff --git a/grabbit/tests/test_core.py b/grabbit/tests/test_core.py
index e85cbe8..d13da59 100644
--- a/grabbit/tests/test_core.py
+++ b/grabbit/tests/test_core.py
@@ -5,6 +5,7 @@ from os.path import join
 import posixpath as psp
 import tempfile
 import json
+from copy import copy
 
 
 DIRNAME = os.path.dirname(__file__)
@@ -64,6 +65,7 @@ class TestFile:
         assert f.entities == {}
 
     def test_matches(self, file):
+        file = copy(file)
         assert file._matches()
         assert file._matches(extensions='nii.gz')
         assert not file._matches(extensions=['.txt', '.rtf'])
@@ -79,6 +81,7 @@ class TestFile:
                              regex_search=True)
 
     def test_named_tuple(self, file):
+        file = copy(file)
         file.tags = {'attrA': Tag(None, 'apple'), 'attrB': Tag(None, 'banana')}
         tup = file.as_named_tuple()
         assert(tup.filename == file.path)
@@ -86,6 +89,15 @@ class TestFile:
         assert not hasattr(tup, 'task')
         assert tup.attrA == 'apple'
 
+    def test_named_tuple_with_reserved_name(self, file):
+        file = copy(file)
+        file.tags['class'] = Tag(None, 'invalid')
+        with pytest.warns(UserWarning) as w:
+            res = file.as_named_tuple()
+            assert w[0].message.args[0].startswith('Entity names cannot')
+            assert hasattr(res, 'class_')
+            assert not hasattr(res, 'class')
+
 
 class TestEntity: