Repository: modin-project/modin

Issue:
BUG: RangeIndex behaves differently than in Pandas
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import pandas as pd
import modin.pandas as md
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000, n_features=10, random_state=1)

X_pd = pd.DataFrame(X)
X_md = md.DataFrame(X)

print(X_pd.loc[pd.RangeIndex(800)])
print(X_md.loc[md.RangeIndex(800)])
```


### Issue Description

I can't select rows from a modin dataframe from a RangeIndex object like I can with pandas.

### Expected Behavior

Return a new dataframe with rows 0 to 799 (800 rows).

### Error Logs

<details>

```python-traceback

Traceback (most recent call last):
  File "C:\repos\ATOM\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3442, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-12-36feb8b7fc9e>", line 1, in <module>
    runfile('C:\\repos\\ATOM\\test.py', wdir='C:\\repos\\ATOM')
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2022.2\plugins\python-ce\helpers\pydev\_pydev_bundle\pydev_umd.py", line 198, in runfile
    pydev_imports.execfile(filename, global_vars, local_vars)  # execute the script
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2022.2\plugins\python-ce\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "C:\repos\ATOM\test.py", line 28, in <module>
    print(X_md.loc[md.RangeIndex(800)])
  File "C:\repos\ATOM\venv\lib\site-packages\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "C:\repos\ATOM\venv\lib\site-packages\modin\pandas\indexing.py", line 719, in __getitem__
    result.index = key
  File "C:\repos\ATOM\venv\lib\site-packages\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "C:\repos\ATOM\venv\lib\site-packages\modin\pandas\dataframe.py", line 2686, in __setattr__
    object.__setattr__(self, key, value)
  File "C:\repos\ATOM\venv\lib\site-packages\modin\pandas\base.py", line 578, in _set_index
    self._query_compiler.index = new_index
  File "C:\repos\ATOM\venv\lib\site-packages\modin\core\storage_formats\pandas\query_compiler.py", line 101, in set_axis
    self._modin_frame.index = idx
  File "C:\repos\ATOM\venv\lib\site-packages\modin\core\dataframe\pandas\dataframe\dataframe.py", line 401, in _set_index
    new_index = self._validate_set_axis(new_index, self._index_cache)
  File "C:\repos\ATOM\venv\lib\site-packages\modin\logging\logger_decorator.py", line 128, in run_and_log
    return obj(*args, **kwargs)
  File "C:\repos\ATOM\venv\lib\site-packages\modin\core\dataframe\pandas\dataframe\dataframe.py", line 351, in _validate_set_axis
    raise ValueError(
ValueError: Length mismatch: Expected axis has 801 elements, new values have 800 elements
(pid=) C:\repos\ATOM\venv\lib\site-packages\ray\dashboard\agent.py:51: DeprecationWarning: There is no current event loop
(pid=)   aiogrpc.init_grpc_aio()


```

</details>


### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit           : 9068fbcfd7d4c169018ae706d6c96edea6db0cd1
python           : 3.10.9.final.0
python-bits      : 64
OS               : Windows
OS-release       : 10
Version          : 10.0.19044
machine          : AMD64
processor        : Intel64 Family 6 Model 126 Stepping 5, GenuineIntel
byteorder        : little
LC_ALL           : None
LANG             : None
Modin dependencies
------------------
modin            : 0.18.1
ray              : 2.2.0
dask             : 2023.1.1
distributed      : 2023.1.1
hdk              : None
pandas dependencies
-------------------
pandas           : 1.5.3
numpy            : 1.23.5
pytz             : 2022.7.1
dateutil         : 2.8.2
setuptools       : 65.5.1
pip              : 23.0.1
Cython           : 0.29.33
pytest           : 7.2.1
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : 4.9.2
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : 3.1.2
IPython          : 8.9.0
pandas_datareader: None
bs4              : 4.11.2
bottleneck       : None
brotli           : 1.0.9
fastparquet      : None
fsspec           : 2023.1.0
gcsfs            : None
matplotlib       : 3.6.3
numba            : 0.56.4
numexpr          : None
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : 10.0.1
pyreadstat       : None
pyxlsb           : None
s3fs             : None
scipy            : 1.9.3
snappy           : None
sqlalchemy       : 1.4.46
tables           : None
tabulate         : 0.9.0
xarray           : None
xlrd             : None
xlwt             : None
zstandard        : None
tzdata           : None

</details>



Discussion:
Apparently, running `X_md.loc[md.RangeIndex(800), :]` does not result in an exception, but instead returns a dataframe with 801 rows, which is definitely not what should happen.
@tvdboom thank you for reporting this bug; I can reproduce it at modin commit d724802391524ad21847722d3d92f8448233f500. I'm looking to see how we can fix it.

Resolving patch:
diff --git a/modin/core/storage_formats/base/query_compiler.py b/modin/core/storage_formats/base/query_compiler.py
--- a/modin/core/storage_formats/base/query_compiler.py
+++ b/modin/core/storage_formats/base/query_compiler.py
@@ -36,7 +36,7 @@
 from modin.utils import MODIN_UNNAMED_SERIES_LABEL, try_cast_to_pandas
 from modin.config import StorageFormat
 
-from pandas.core.dtypes.common import is_scalar
+from pandas.core.dtypes.common import is_scalar, is_number
 import pandas.core.resample
 import pandas
 from pandas._typing import IndexLabel, Suffixes
@@ -3391,14 +3391,27 @@ def get_positions_from_labels(self, row_loc, col_loc):
         for axis, axis_loc in enumerate((row_loc, col_loc)):
             if is_scalar(axis_loc):
                 axis_loc = np.array([axis_loc])
-            if isinstance(axis_loc, slice) or is_range_like(axis_loc):
+            if isinstance(axis_loc, pandas.RangeIndex):
+                axis_lookup = axis_loc
+            elif isinstance(axis_loc, slice) or is_range_like(axis_loc):
                 if isinstance(axis_loc, slice) and axis_loc == slice(None):
                     axis_lookup = axis_loc
                 else:
                     axis_labels = self.get_axis(axis)
                     # `slice_indexer` returns a fully-defined numeric slice for a non-fully-defined labels-based slice
+                    # RangeIndex and range use a semi-open interval, while
+                    # slice_indexer uses a closed interval. Subtract 1 step from the
+                    # end of the interval to get the equivalent closed interval.
+                    if axis_loc.stop is None or not is_number(axis_loc.stop):
+                        slice_stop = axis_loc.stop
+                    else:
+                        slice_stop = axis_loc.stop - (
+                            0 if axis_loc.step is None else axis_loc.step
+                        )
                     axis_lookup = axis_labels.slice_indexer(
-                        axis_loc.start, axis_loc.stop, axis_loc.step
+                        axis_loc.start,
+                        slice_stop,
+                        axis_loc.step,
                     )
                     # Converting negative indices to their actual positions:
                     axis_lookup = pandas.RangeIndex(


Test patch:
diff --git a/modin/pandas/test/dataframe/test_indexing.py b/modin/pandas/test/dataframe/test_indexing.py
--- a/modin/pandas/test/dataframe/test_indexing.py
+++ b/modin/pandas/test/dataframe/test_indexing.py
@@ -840,6 +840,48 @@ def test_loc_iloc_slice_indexer(locator_name, slice_indexer):
     eval_general(md_df, pd_df, lambda df: getattr(df, locator_name)[slice_indexer])
 
 
+@pytest.mark.parametrize(
+    "indexer_size",
+    [
+        1,
+        2,
+        NROWS,
+        pytest.param(
+            NROWS + 1,
+            marks=pytest.mark.xfail(
+                reason="https://github.com/modin-project/modin/issues/5739", strict=True
+            ),
+        ),
+    ],
+)
+class TestLocRangeLikeIndexer:
+    """Test cases related to https://github.com/modin-project/modin/issues/5702"""
+
+    def test_range_index_getitem_single_value(self, indexer_size):
+        eval_general(
+            *create_test_dfs(test_data["int_data"]),
+            lambda df: df.loc[pd.RangeIndex(indexer_size)],
+        )
+
+    def test_range_index_getitem_two_values(self, indexer_size):
+        eval_general(
+            *create_test_dfs(test_data["int_data"]),
+            lambda df: df.loc[pd.RangeIndex(indexer_size), :],
+        )
+
+    def test_range_getitem_single_value(self, indexer_size):
+        eval_general(
+            *create_test_dfs(test_data["int_data"]),
+            lambda df: df.loc[range(indexer_size)],
+        )
+
+    def test_range_getitem_two_values_5702(self, indexer_size):
+        eval_general(
+            *create_test_dfs(test_data["int_data"]),
+            lambda df: df.loc[range(indexer_size), :],
+        )
+
+
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
 def test_pop(request, data):
     modin_df = pd.DataFrame(data)