Repository: modin-project/modin

Issue:
Slicing small Series objects throws Internal Error exception

### Describe the problem
<!-- Describe the problem clearly here. -->
When working with small Series objects, slicing causing internal metadata to be miscalculated.

### Source code / logs

Works:
```python
c = pd.Series(['', 'a', 'b', 'c', 'a'] * 1000) 
c[3:7]
```

Doesn't work:
```python
c = pd.Series(['', 'a', 'b', 'c', 'a']) 
c[3:7]
```

Logs:
```
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-6-525f4cb2bfb2> in <module>
----> 1 c[3:7]

~/software_builds/modin/modin/pandas/base.py in __getitem__(self, key)
   3254         )
   3255         if indexer is not None:
-> 3256             return self._getitem_slice(indexer)
   3257         else:
   3258             return self._getitem(key)

~/software_builds/modin/modin/pandas/base.py in _getitem_slice(self, key)
   3283             elif key.stop is None:
   3284                 return self.tail(compute_offset(-key.start))
-> 3285             return self.head(compute_offset(key.stop)).tail(compute_offset(-key.start))
   3286         # We convert to a RangeIndex because getitem_row_array is expecting a list
   3287         # of indices, and RangeIndex will give us the exact indices of each boolean

~/software_builds/modin/modin/pandas/series.py in tail(self, n)
   1052         if n == 0:
   1053             return Series(dtype=self.dtype)
-> 1054         return super(Series, self).tail(n)
   1055 
   1056     def take(self, indices, axis=0, is_copy=None, **kwargs):

~/software_builds/modin/modin/pandas/base.py in tail(self, n)
   2789         if n >= len(self.index):
   2790             return self.copy()
-> 2791         return self.__constructor__(query_compiler=self._query_compiler.tail(n))
   2792 
   2793     def to_clipboard(self, excel=True, sep=None, **kwargs):  # pragma: no cover

~/software_builds/modin/modin/backends/pandas/query_compiler.py in tail(self, n)
    808             QueryCompiler containing the last n rows of the original QueryCompiler.
    809         """
--> 810         return self.__constructor__(self._modin_frame.tail(n))
    811 
    812     def front(self, n):

~/software_builds/modin/modin/engines/base/frame/data.py in tail(self, n)
   1355             new_row_lengths,
   1356             self._column_widths,
-> 1357             self._dtypes,
   1358         )
   1359 

~/software_builds/modin/modin/engines/base/frame/data.py in __init__(self, partitions, index, columns, row_lengths, column_widths, dtypes)
     62                 sum(row_lengths) != len(self._index_cache),
     63                 "Row lengths: {} != {}".format(
---> 64                     sum(row_lengths), len(self._index_cache)
     65                 ),
     66             )

~/software_builds/modin/modin/error_message.py in catch_bugs_and_request_email(cls, failure_condition, extra_log)
     51                 "Internal Error. "
     52                 "Please email bug_reports@modin.org with the traceback and command that"
---> 53                 " caused this error.\n{}".format(extra_log)
     54             )
     55 

Exception: Internal Error. Please email bug_reports@modin.org with the traceback and command that caused this error.
Row lengths: 0 != 2
```
<!-- Include any logs or source code that would be helpful to diagnose the problem. If including tracebacks, please include the full traceback. Large logs and files should be attached. Try to provide a reproducible test case that is the bare minimum necessary to generate the problem. -->



Discussion:

Resolving patch:
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -3258,51 +3258,9 @@ def __getitem__(self, key):
             return self._getitem(key)
 
     def _getitem_slice(self, key):
-        # If there is no step, we can fasttrack the codepath to use existing logic from
-        # head and tail, which is already pretty fast.
-        if (
-            key.step is None
-            and (isinstance(key.start, int) or key.start is None)
-            and (isinstance(key.stop, int) or key.stop is None)
-        ):
-            if key.start is None and key.stop is None:
-                return self.copy()
-
-            def compute_offset(value):
-                return (
-                    value - len(self)
-                    if value > 0
-                    else value
-                    if value != 0
-                    else len(self)
-                )
-
-            # Head is a negative number, Tail is a positive number
-            if key.start is None:
-                return self.head(compute_offset(key.stop))
-            elif key.stop is None:
-                return self.tail(compute_offset(-key.start))
-            return self.head(compute_offset(key.stop)).tail(compute_offset(-key.start))
-        # We convert to a RangeIndex because getitem_row_array is expecting a list
-        # of indices, and RangeIndex will give us the exact indices of each boolean
-        # requested.
-        if isinstance(key.start, int) and isinstance(key.stop, int):
-            key = pandas.RangeIndex(len(self.index))[key]
-        else:
-            # To handle this case correctly, we let pandas compute the indices by
-            # creating a MultiIndex ("a", "b") such that "a" is the original index and
-            # "b" is the Range of numeric indices for each. Then we apply the slice to
-            # the original index (named "a"), then extract the index values (named "b")
-            # and pass that along to be sliced in the actual data. This also serves as a
-            # validation that the input slice is usable and correct.
-            key = pandas.DataFrame(
-                index=pandas.MultiIndex.from_arrays(
-                    [self.index, pandas.RangeIndex(len(self.index))], names=["a", "b"]
-                )
-            )[key].index.get_level_values("b")
-        return self.__constructor__(
-            query_compiler=self._query_compiler.getitem_row_array(key)
-        )
+        if key.start is None and key.stop is None:
+            return self.copy()
+        return self.iloc[key]
 
     def __getstate__(self):
         return self._default_to_pandas("__getstate__")


Test patch:
diff --git a/modin/pandas/test/test_series.py b/modin/pandas/test/test_series.py
--- a/modin/pandas/test/test_series.py
+++ b/modin/pandas/test/test_series.py
@@ -338,6 +338,23 @@ def test___getitem__(data):
     df_equals(pd.Series([])[:30], pandas.Series([])[:30])
 
 
+def test___getitem__1383():
+    # see #1383 for more details
+    data = ["", "a", "b", "c", "a"]
+    modin_series = pd.Series(data)
+    pandas_series = pandas.Series(data)
+    df_equals(modin_series[3:7], pandas_series[3:7])
+
+
+@pytest.mark.parametrize("start", [-7, -5, -3, 0, None, 3, 5, 7])
+@pytest.mark.parametrize("stop", [-7, -5, -3, 0, None, 3, 5, 7])
+def test___getitem_edge_cases(start, stop):
+    data = ["", "a", "b", "c", "a"]
+    modin_series = pd.Series(data)
+    pandas_series = pandas.Series(data)
+    df_equals(modin_series[start:stop], pandas_series[start:stop])
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test___gt__(data):
     modin_series, pandas_series = create_test_series(data)