Repository: pydata/sparse

Issue:
Can't use COO initializer with coords set to an empty list to create a scalar
**Describe the bug**
Version 0.10 of the sparse package broke the ability to create a scalar using the `COO` initializer when passing an empty list as the coords.

**To Reproduce**

    # these don't work
    zero = COO([], [], ())
    one = COO([], [1], ())

    # these do
    zero = COO(np.empty((0,0), [], ())
    one = COO(np.empty((0,1), [1], ())

**Expected behavior**
All four lines above should create COO objects representing scalars (and this was indeed the behavior before version 0.10).  Instead, the first two lines throw an exception, while only the last two succeed.

Technically the documentation says that the coords argument should be an ndarray, but the documentation shows a list being used , so I think using an empty list to construct a scalar should be supported.

**System**
 - OS and version: Windows 10
 - `sparse` version: 0.10
 - NumPy version: 1.18.5
 - Numba version: 0.49.1

**Additional context**
Here's a quick preliminary analysis of what I think is happening.

[Here](https://github.com/pydata/sparse/blob/03ba77b398bdd874063f18b0dd2a7701f0708086/sparse/_coo/core.py#L224), in the COO initializer, there's a check for whether `coords` is 1D in which case it's promoted to 2D (with a singleton first dimension).  But if shape is `()`, then an empty list seems like a reasonable value for `coords` (but will be a 1D array of length 0 after `np.asarray` has been applied to it).  In this case, the promotion to 2D should happen differently: it should result in an array of shape either (0,0) or (0,1) depending on whether data is emtpy or has a single entry.

Later, the COO initializer calls `_sort_indices`, which calls `self.linear_loc()`, which calls `common.linear_loc`, but because the `coords` input wrongly has dimension `(1,0)` the check [here](https://github.com/pydata/sparse/blob/03ba77b398bdd874063f18b0dd2a7701f0708086/sparse/_coo/common.py#L57) never succeeds and the wrong branch of the logic is chosen.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydata/sparse/pull/356?src=pr&el=h1) Report
> Merging [#356](https://codecov.io/gh/pydata/sparse/pull/356?src=pr&el=desc) into [master](https://codecov.io/gh/pydata/sparse/commit/2f119f6c974680fde89709babfd79395ba1198b8&el=desc) will **decrease** coverage by `0.08%`.
> The diff coverage is `100.00%`.


```diff
@@            Coverage Diff             @@
##           master     #356      +/-   ##
==========================================
- Coverage   94.12%   94.03%   -0.09%     
==========================================
  Files          19       19              
  Lines        2418     2382      -36     
==========================================
- Hits         2276     2240      -36     
  Misses        142      142              
```




Patch:
diff --git a/sparse/_coo/core.py b/sparse/_coo/core.py
index 2d86be9..0f6c661 100644
--- a/sparse/_coo/core.py
+++ b/sparse/_coo/core.py
@@ -222,7 +222,10 @@ class COO(SparseArray, NDArrayOperatorsMixin):  # lgtm [py/missing-equals]
         self.coords = np.asarray(coords)
 
         if self.coords.ndim == 1:
-            self.coords = self.coords[None, :]
+            if self.coords.size == 0 and shape is not None:
+                self.coords = self.coords.reshape((len(shape), len(data)))
+            else:
+                self.coords = self.coords[None, :]
 
         if self.data.ndim == 0:
             self.data = np.broadcast_to(self.data, self.coords.shape[1])


Test patch:
diff --git a/sparse/tests/test_coo.py b/sparse/tests/test_coo.py
index 762affb..ae77f63 100644
--- a/sparse/tests/test_coo.py
+++ b/sparse/tests/test_coo.py
@@ -2330,3 +2330,11 @@ def test_outer(shape1, shape2):
 
     assert_eq(sparse.outer(s1, s2), np.outer(x1, x2))
     assert_eq(np.multiply.outer(s1, s2), np.multiply.outer(x1, x2))
+
+
+def test_scalar_list_init():
+    a = sparse.COO([], [], ())
+    b = sparse.COO([], [1], ())
+
+    assert a.todense() == 0
+    assert b.todense() == 1