Repository: numba/numba

Issue:
Support alternative spellings of dtypes in dtype kwarg of numpy array constructors
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

Since #6262 some of the commonly used array constructors from NumPy, e.g. `np.zeros` have supported string literals in the `dtype` kwarg. There's a minor bug in the typing at present which means that the `dtype` spelling has to be an attribute on the `numpy` top level module, e.g. `np.int64`. This sort of thing doesn't work:
```python
import numpy as np
from numba import njit

@njit
def foo():
    return np.zeros((1, 2), dtype='i8')

foo()
```
fix is along the lines of:
```patch
diff --git a/numba/core/typing/npydecl.py b/numba/core/typing/npydecl.py
index 50cff1c6a..f84f483fb 100644
--- a/numba/core/typing/npydecl.py
+++ b/numba/core/typing/npydecl.py
@@ -440,6 +440,11 @@ def parse_dtype(dtype):
         return dtype.instance_type
     elif isinstance(dtype, types.StringLiteral):
         dt = getattr(np, dtype.literal_value, None)
+        if dt is None: # try again with an actual dtype
+            try:
+                dt = np.dtype(dtype.literal_value).type
+            except ValueError:
+                pass
         if dt is not None:
             return from_dtype(dt)
```
Support alternative spellings of dtypes in dtype kwarg of numpy array constructors
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [x] I have tried using the latest released version of Numba (most recent is
 visible in the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [x] I have included a self contained code sample to reproduce the problem.
  i.e. it's possible to run as 'python bug.py'.

Since #6262 some of the commonly used array constructors from NumPy, e.g. `np.zeros` have supported string literals in the `dtype` kwarg. There's a minor bug in the typing at present which means that the `dtype` spelling has to be an attribute on the `numpy` top level module, e.g. `np.int64`. This sort of thing doesn't work:
```python
import numpy as np
from numba import njit

@njit
def foo():
    return np.zeros((1, 2), dtype='i8')

foo()
```
fix is along the lines of:
```patch
diff --git a/numba/core/typing/npydecl.py b/numba/core/typing/npydecl.py
index 50cff1c6a..f84f483fb 100644
--- a/numba/core/typing/npydecl.py
+++ b/numba/core/typing/npydecl.py
@@ -440,6 +440,11 @@ def parse_dtype(dtype):
         return dtype.instance_type
     elif isinstance(dtype, types.StringLiteral):
         dt = getattr(np, dtype.literal_value, None)
+        if dt is None: # try again with an actual dtype
+            try:
+                dt = np.dtype(dtype.literal_value).type
+            except ValueError:
+                pass
         if dt is not None:
             return from_dtype(dt)
```


Discussion:

Resolving patch:
diff --git a/numba/core/typing/npydecl.py b/numba/core/typing/npydecl.py
--- a/numba/core/typing/npydecl.py
+++ b/numba/core/typing/npydecl.py
@@ -439,9 +439,13 @@ def parse_dtype(dtype):
     elif isinstance(dtype, types.TypeRef):
         return dtype.instance_type
     elif isinstance(dtype, types.StringLiteral):
-        dt = getattr(np, dtype.literal_value, None)
-        if dt is not None:
-            return from_dtype(dt)
+        dtstr = dtype.literal_value
+        try:
+            dt = np.dtype(dtstr)
+        except TypeError:
+            msg = f"Invalid NumPy dtype specified: '{dtstr}'"
+            raise TypingError(msg)
+        return from_dtype(dt)
 
 def _parse_nested_sequence(context, typ):
     """
diff --git a/numba/np/numpy_support.py b/numba/np/numpy_support.py
--- a/numba/np/numpy_support.py
+++ b/numba/np/numpy_support.py
@@ -95,13 +95,18 @@ def from_dtype(dtype):
     try:
         return FROM_DTYPE[dtype]
     except KeyError:
-        char = dtype.char
+        pass
 
+    try:
+        char = dtype.char
+    except AttributeError:
+        pass
+    else:
         if char in 'SU':
             return _from_str_dtype(dtype)
         if char in 'mM':
             return _from_datetime_dtype(dtype)
-        if char in 'V':
+        if char in 'V' and dtype.subdtype is not None:
             subtype = from_dtype(dtype.subdtype[0])
             return types.NestedArray(subtype, dtype.shape)
 


Test patch:
diff --git a/numba/tests/test_dyn_array.py b/numba/tests/test_dyn_array.py
--- a/numba/tests/test_dyn_array.py
+++ b/numba/tests/test_dyn_array.py
@@ -625,6 +625,26 @@ def func(n):
             return pyfunc(n, 'complex128')
         self.check_1d(func)
 
+    def test_1d_dtype_str_alternative_spelling(self):
+        # like test_1d_dtype_str but using the shorthand type spellings
+        pyfunc = self.pyfunc
+        _dtype = 'i4'
+        def func(n):
+            return pyfunc(n, _dtype)
+        self.check_1d(func)
+
+        def func(n):
+            return pyfunc(n, 'c8')
+        self.check_1d(func)
+
+    def test_1d_dtype_str_structured_dtype(self):
+        # test_1d_dtype_str but using a structured dtype
+        pyfunc = self.pyfunc
+        _dtype = "i4, (2,3)f8"
+        def func(n):
+            return pyfunc(n, _dtype)
+        self.check_1d(func)
+
     def test_1d_dtype_non_const_str(self):
         pyfunc = self.pyfunc
 
@@ -641,6 +661,19 @@ def func(n, dt):
         regex = re.compile(restr.format(pyfunc.__name__))
         self.assertRegex(excstr, regex)
 
+    def test_1d_dtype_invalid_str(self):
+        pyfunc = self.pyfunc
+
+        @njit
+        def func(n):
+            return pyfunc(n, 'ABCDEF')
+
+        with self.assertRaises(TypingError) as raises:
+            func(5)
+
+        excstr = str(raises.exception)
+        self.assertIn("Invalid NumPy dtype specified: 'ABCDEF'", excstr)
+
     def test_2d(self):
         pyfunc = self.pyfunc
         def func(m, n):
@@ -675,6 +708,13 @@ def func(m, n):
             return pyfunc((m, n), dtype='complex64')
         self.check_2d(func)
 
+    def test_2d_dtype_str_kwarg_alternative_spelling(self):
+        # as test_2d_dtype_str_kwarg but with the numpy shorthand type spelling
+        pyfunc = self.pyfunc
+        def func(m, n):
+            return pyfunc((m, n), dtype='c8')
+        self.check_2d(func)
+
     def test_alloc_size(self):
         pyfunc = self.pyfunc
         width = types.intp.bitwidth
@@ -691,6 +731,10 @@ def setUp(self):
         super(TestNdOnes, self).setUp()
         self.pyfunc = np.ones
 
+    @unittest.expectedFailure
+    def test_1d_dtype_str_structured_dtype(self):
+        super().test_1d_dtype_str_structured_dtype()
+
 
 class TestNdFull(ConstructorBaseTest, TestCase):
 
@@ -723,6 +767,12 @@ def func(n):
             return np.full(n, 4.5, 'bool_')
         self.check_1d(func)
 
+    def test_1d_dtype_str_alternative_spelling(self):
+        # like test_1d_dtype_str but using the shorthand type spelling
+        def func(n):
+            return np.full(n, 4.5, '?')
+        self.check_1d(func)
+
     def test_1d_dtype_non_const_str(self):
 
         @njit
@@ -738,6 +788,18 @@ def func(n, fv, dt):
         regex = re.compile(restr)
         self.assertRegex(excstr, regex)
 
+    def test_1d_dtype_invalid_str(self):
+
+        @njit
+        def func(n, fv):
+            return np.full(n, fv, 'ABCDEF')
+
+        with self.assertRaises(TypingError) as raises:
+            func(np.ones(4), 4.5)
+
+        excstr = str(raises.exception)
+        self.assertIn("Invalid NumPy dtype specified: 'ABCDEF'", excstr)
+
     def test_2d(self):
         def func(m, n):
             return np.full((m, n), 4.5)
@@ -888,10 +950,10 @@ def func(arr):
             return pyfunc(arr, dtype='int32')
         self.check_like(func, np.float64)
 
-    def test_like_dtype_str_kwarg(self):
+    def test_like_dtype_str_kwarg_alternative_spelling(self):
         pyfunc = self.pyfunc
         def func(arr):
-            return pyfunc(arr, dtype='int32')
+            return pyfunc(arr, dtype='i4')
         self.check_like(func, np.float64)
 
     def test_like_dtype_non_const_str(self):
@@ -911,6 +973,19 @@ def func(n, dt):
             '{}(array(float64, 1d, C), unicode_type)'.format(pyfunc.__name__),
             excstr)
 
+    def test_like_dtype_invalid_str(self):
+        pyfunc = self.pyfunc
+
+        @njit
+        def func(n):
+            return pyfunc(n, 'ABCDEF')
+
+        with self.assertRaises(TypingError) as raises:
+            func(np.ones(4))
+
+        excstr = str(raises.exception)
+        self.assertIn("Invalid NumPy dtype specified: 'ABCDEF'", excstr)
+
 
 class TestNdZerosLike(TestNdEmptyLike):
 
@@ -985,6 +1060,11 @@ def func(arr):
             return np.full_like(arr, 4.5, 'bool_')
         self.check_like(func, np.float64)
 
+    def test_like_dtype_str_kwarg_alternative_spelling(self):
+        def func(arr):
+            return np.full_like(arr, 4.5, dtype='?')
+        self.check_like(func, np.float64)
+
     def test_like_dtype_non_const_str_kwarg(self):
 
         @njit
@@ -999,6 +1079,18 @@ def func(arr, fv, dt):
         self.assertIn('full_like(array(float64, 1d, C), float64, unicode_type)',
                       excstr)
 
+    def test_like_dtype_invalid_str(self):
+
+        @njit
+        def func(arr, fv):
+            return np.full_like(arr, fv, "ABCDEF")
+
+        with self.assertRaises(TypingError) as raises:
+            func(np.ones(4), 3.4)
+
+        excstr = str(raises.exception)
+        self.assertIn("Invalid NumPy dtype specified: 'ABCDEF'", excstr)
+
 
 class TestNdIdentity(BaseTest):