Repository: numba/numba
Pull request: NamedTuple - Raises an error on non-iterable elements
Fix #4369

I still need to write some tests.

Problem statement:
NamedTuple.__init__ hangs with second argument as literal
Tested on '0.45.0' and '0.46.0dev0+5.gfddbdc5'

The following piece of code hangs forever when calling `NamedTupe.__init__` when second argument is a literal.

This should probably raise an error?

```python
from numba import types


types.NamedTuple(types.uint32, 2)
```

Hints from issue discussion:
Thanks for the report. I don't think that this is the correct way to instantiate a `types.NamedTuple`, the first argument should be an iterable of types, the second a `namedtuple` type. Example:

```pycon
In [9]: from collections import namedtuple                                                                                      

In [10]: from numba import types                                                                                                

In [11]: f = namedtuple('f', 'a b')                                                                                             

In [12]: types.NamedTuple((types.uint32, types.float64), f)                                                                     
Out[12]: f(uint32, float64)

In [13]: z = types.NamedTuple((types.uint32, types.float64), f)                                                                 

In [14]: type(z)                                                                                                                
Out[14]: numba.types.containers.NamedTuple
```
Thanks for the response. I was expecting an exception being raised for the invalid usage instead of a hang.
Similarly to what happens when a ```types.NamedUniTuple``` is incorrectly instantiated:

```python
In [7]: types.NamedUniTuple(types.uint32, 2, 2)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-7-64339c48032d> in <module>
----> 1 types.NamedUniTuple(types.uint32, 2, 2)

~\Anaconda3\lib\site-packages\numba\types\abstract.py in __call__(cls, *args, **kwargs)
     58         the new instance is returned.
     59         """
---> 60         inst = type.__call__(cls, *args, **kwargs)
     61         return cls._intern(inst)
     62

~\Anaconda3\lib\site-packages\numba\types\containers.py in __init__(self, dtype, count, cls)
    292         self.dtype = dtype
    293         self.count = count
--> 294         self.fields = tuple(cls._fields)
    295         self.instance_class = cls
    296         name = "%s(%s x %d)" % (cls.__name__, dtype, count)

AttributeError: 'int' object has no attribute '_fields'
```
I think that an exception is raised in the second case is entirely an accident, just a consequence of duck typing. IIRC the type constructors have no safety guarantees and are only generally used by developers/more advanced users, obviously it'd be great to strengthen their safety. Will mark this as a bug and a good second issue as I think someone familiar with the Numba types could help with this.

Commit message: Update test_tuples.py

Patch to review:
diff --git a/numba/tests/test_tuples.py b/numba/tests/test_tuples.py
index ff15a892164..f46058636c1 100644
--- a/numba/tests/test_tuples.py
+++ b/numba/tests/test_tuples.py
@@ -361,6 +361,16 @@ def test_le(self):
 
 class TestNamedTuple(TestCase, MemoryLeakMixin):
 
+    def test_non_iterable_type_exception(self):
+        self.disable_leak_check()
+
+        with self.assertRaises(errors.TypingError) as raises:
+            types.NamedTuple(types.uint32, 'p')
+            self.assertIn(
+                "<class 'numba.types.scalars.Integer'> type is not iterable",
+                str(raises.exception)
+            )
+
     def test_unpack(self):
         def check(p):
             for pyfunc in tuple_first, tuple_second:
diff --git a/numba/types/containers.py b/numba/types/containers.py
index 590834ac6c3..33ab0aeb039 100644
--- a/numba/types/containers.py
+++ b/numba/types/containers.py
@@ -1,5 +1,7 @@
 from __future__ import print_function, division, absolute_import
 
+from collections import Iterable
+
 from .abstract import *
 from .common import *
 from .misc import Undefined, unliteral, Optional, NoneType
@@ -308,6 +310,10 @@ def key(self):
 class NamedTuple(_HeterogeneousTuple, BaseNamedTuple):
 
     def __init__(self, types, cls):
+
+        if not isinstance(types, Iterable):
+            raise TypingError("{} type is not iterable".format(type(types)))
+
         self.types = tuple(types)
         self.count = len(self.types)
         self.fields = tuple(cls._fields)


Code review comments:
diff_hunk: @@ -308,6 +310,10 @@ def key(self):
 class NamedTuple(_HeterogeneousTuple, BaseNamedTuple):
 
     def __init__(self, types, cls):
+
+        if not isinstance(types, Iterable):
+            raise TypingError("{} type is not iterable".format(type(types)))
path: numba/types/containers.py
text: @user1:
I think this check would be better placed in the `_HeterogeneousTuple` class as a method and then it be called by the `__init__` of both `Tuple` and `NamedTuple` as they both require an `Iterable` for the `types` argument. Further this error message perhaps should read:
```
Argument 'types' must be an Iterable.
```
so as to explain what the requirement is and that it relates to the `types` argument.

Following this tests for all the `*Tuple*` constructors accepting an `Iterable` types can be written.

@author:
For `Tuple` I had to put the check on `__new__` since there is a conditional there using `types`.