Repository: modin-project/modin

Issue:
BUG: printing dataframe with virtual partitions can raise AttributeError: 'RangeIndex' object has no attribute 'iloc'
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: MacOS Monterey 12.1
- **Modin version** (`modin.__version__`): 3f985ed6864cc1b5b587094d75ca5b2695e4139f
- **Python version**: 3.10.2
- **Code we can use to reproduce**:

```python
import modin.pandas as pd
from modin.config import NPartitions

NPartitions.put(16)
print(pd.concat([pd.DataFrame([i]) for i in range(40)]))
```

### Describe the problem
I get an error ending with `'RangeIndex' object has no attribute 'iloc' ` when I run the code above.

If `rebalance_partitions` after the concat ends up making one final partition with only a single row, you get the bug, because in this code:

https://github.com/modin-project/modin/blob/3f985ed6864cc1b5b587094d75ca5b2695e4139f/modin/core/dataframe/pandas/partitioning/axis_partition.py#L144-L154

`num_splits == len(partitions) == 1 ` so we set `lengths = [1]` instead of `None`. We then hit this buggy code trying to split an index object instead of a dataframe in `split_result_of_axis_func_pandas`:

https://github.com/modin-project/modin/blob/8864bc197974da6d8cda2de2f35ca31d561be1cc/modin/core/storage_formats/pandas/utils.py#L90-L96


### Source code / logs
<details>
<summary>Main thread error</summary>

```
RayTaskError(AttributeError)              Traceback (most recent call last)
Input In [1], in <cell line: 5>()
      2 from modin.config import NPartitions
      4 NPartitions.put(16)
----> 5 print(pd.concat([pd.DataFrame([i]) for i in range(40)]))

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/pandas/base.py:3373, in BasePandasDataset.__str__(self)
   3365 def __str__(self):  # pragma: no cover
   3366     """
   3367     Return str(self).
   3368
   (...)
   3371     str
   3372     """
-> 3373     return repr(self)

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/pandas/dataframe.py:223, in DataFrame.__repr__(self)
    220         i -= 1
    222     num_cols += len(self.columns) - i
--> 223 result = repr(self._build_repr_df(num_rows, num_cols))
    224 if len(self.index) > num_rows or len(self.columns) > num_cols:
    225     # The split here is so that we don't repr pandas row lengths.
    226     return result.rsplit("\n\n", 1)[0] + "\n\n[{0} rows x {1} columns]".format(
    227         len(self.index), len(self.columns)
    228     )

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/pandas/base.py:198, in BasePandasDataset._build_repr_df(self, num_rows, num_cols)
    196 else:
    197     indexer = row_indexer
--> 198 return self.iloc[indexer]._query_compiler.to_pandas()

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/pandas/indexing.py:961, in _iLocIndexer.__getitem__(self, key)
    957     return self._handle_boolean_masking(row_loc, col_loc)
    959 row_lookup, col_lookup = self._compute_lookup(row_loc, col_loc)
--> 961 result = self._getitem_positional(
    962     row_lookup,
    963     col_lookup,
    964     row_multiindex_full_lookup=False,
    965     col_multiindex_full_lookup=False,
    966     row_scalar=row_scalar,
    967     col_scalar=col_scalar,
    968     ndim=ndim,
    969 )
    971 if isinstance(result, Series):
    972     result._parent = self.df

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/pandas/indexing.py:377, in _LocationIndexerBase._getitem_positional(self, row_lookup, col_lookup, row_multiindex_full_lookup, col_multiindex_full_lookup, row_scalar, col_scalar, ndim)
    371     ErrorMessage.catch_bugs_and_request_email(
    372         failure_condition=col_lookup != slice(None),
    373         extra_log=f"Only None-slices are acceptable as a slice argument in masking, got: {col_lookup}",
    374     )
    375     col_lookup = None
--> 377 qc_view = self.qc.view(row_lookup, col_lookup)
    379 if ndim == 2:
    380     return self.df.__constructor__(query_compiler=qc_view)

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/storage_formats/pandas/query_compiler.py:3119, in PandasQueryCompiler.view(self, index, columns)
   3117 def view(self, index=None, columns=None):
   3118     return self.__constructor__(
-> 3119         self._modin_frame.mask(row_positions=index, col_positions=columns)
   3120     )

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:119, in lazy_metadata_decorator.<locals>.decorator.<locals>.run_f_on_minimally_updated_metadata(self, *args, **kwargs)
    117     elif apply_axis == "rows":
    118         obj._propagate_index_objs(axis=0)
--> 119 result = f(self, *args, **kwargs)
    120 if apply_axis is None and not transpose:
    121     result._deferred_index = self._deferred_index

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:674, in PandasDataframe.mask(self, row_labels, row_positions, col_labels, col_positions)
    666 row_positions, col_positions = indexers
    668 if (
    669     col_labels is None
    670     and col_positions is None
    671     and row_labels is None
    672     and row_positions is None
    673 ):
--> 674     return self.copy()
    675 # Get numpy array of positions of values from `row_labels`
    676 if row_labels is not None:

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:119, in lazy_metadata_decorator.<locals>.decorator.<locals>.run_f_on_minimally_updated_metadata(self, *args, **kwargs)
    117     elif apply_axis == "rows":
    118         obj._propagate_index_objs(axis=0)
--> 119 result = f(self, *args, **kwargs)
    120 if apply_axis is None and not transpose:
    121     result._deferred_index = self._deferred_index

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:1005, in PandasDataframe.copy(self)
    991 @lazy_metadata_decorator(apply_axis=None)
    992 def copy(self):
    993     """
    994     Copy this object.
    995
   (...)
    999         A copied version of this object.
   1000     """
   1001     return self.__constructor__(
   1002         self._partitions,
   1003         self.index.copy(),
   1004         self.columns.copy(),
-> 1005         self._row_lengths,
   1006         self._column_widths,
   1007         self._dtypes,
   1008     )

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:248, in PandasDataframe._row_lengths(self)
    243 if self._row_lengths_cache is None:
    244     if len(self._partitions) > 0:
    245         (
    246             index,
    247             self._row_lengths_cache,
--> 248         ) = self._compute_axis_labels_and_lengths(0)
    249         if self._index_cache is None:
    250             self._index_cache = index

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/dataframe/pandas/dataframe/dataframe.py:460, in PandasDataframe._compute_axis_labels_and_lengths(self, axis, partitions)
    458 if partitions is None:
    459     partitions = self._partitions
--> 460 new_index, internal_idx = self._partition_mgr_cls.get_indices(axis, partitions)
    461 return new_index, list(map(len, internal_idx))

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/dataframe/pandas/partitioning/partition_manager.py:864, in PandasDataframePartitionManager.get_indices(cls, axis, partitions, index_func)
    862 target = partitions.T if axis == 0 else partitions
    863 new_idx = [idx.apply(func) for idx in target[0]] if len(target) else []
--> 864 new_idx = cls.get_objects_from_partitions(new_idx)
    865 # TODO FIX INFORMATION LEAK!!!!1!!1!!
    866 total_idx = new_idx[0].append(new_idx[1:]) if new_idx else new_idx

File ~/software_sources/modin/modin/logging/logger_decorator.py:128, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    113 """
    114 Compute function with logging if Modin logging is enabled.
    115
   (...)
    125 Any
    126 """
    127 if LogMode.get() == "disable":
--> 128     return obj(*args, **kwargs)
    130 logger = get_logger()
    131 logger_level = getattr(logger, log_level)

File ~/software_sources/modin/modin/core/execution/ray/implementations/pandas_on_ray/partitioning/partition_manager.py:110, in PandasOnRayDataframePartitionManager.get_objects_from_partitions(cls, partitions)
     95 @classmethod
     96 def get_objects_from_partitions(cls, partitions):
     97     """
     98     Get the objects wrapped by `partitions` in parallel.
     99
   (...)
    108         The objects wrapped by `partitions`.
    109     """
--> 110     return ray.get([partition._data for partition in partitions])

File ~/opt/anaconda3/envs/modin-dev/lib/python3.10/site-packages/ray/_private/client_mode_hook.py:105, in client_mode_hook.<locals>.wrapper(*args, **kwargs)
    103     if func.__name__ != "init" or is_client_mode_enabled_by_default:
    104         return getattr(ray, func.__name__)(*args, **kwargs)
--> 105 return func(*args, **kwargs)

File ~/opt/anaconda3/envs/modin-dev/lib/python3.10/site-packages/ray/worker.py:1831, in get(object_refs, timeout)
   1829     worker.core_worker.dump_object_store_memory_usage()
   1830 if isinstance(value, RayTaskError):
-> 1831     raise value.as_instanceof_cause()
   1832 else:
   1833     raise value
```
</details>


<details>
<summary>Ray error</summary>

```
RayTaskError(AttributeError): ray::deploy_ray_func() (pid=31458, ip=127.0.0.1)
  File "/Users/maheshvashishtha/software_sources/modin/modin/core/execution/ray/implementations/pandas_on_ray/partitioning/virtual_partition.py", line 548, in deploy_ray_func
    result = func(*args, **kwargs)
  File "/Users/maheshvashishtha/software_sources/modin/modin/core/dataframe/pandas/partitioning/axis_partition.py", line 155, in deploy_axis_func
    return split_result_of_axis_func_pandas(axis, num_splits, result, lengths)
  File "/Users/maheshvashishtha/software_sources/modin/modin/core/storage_formats/pandas/utils.py", line 94, in split_result_of_axis_func_pandas
    return [result.iloc[sums[i] : sums[i + 1]] for i in range(len(sums) - 1)]
  File "/Users/maheshvashishtha/software_sources/modin/modin/core/storage_formats/pandas/utils.py", line 94, in <listcomp>
    return [result.iloc[sums[i] : sums[i + 1]] for i in range(len(sums) - 1)]
AttributeError: 'RangeIndex' object has no attribute 'iloc'

```
</details>



Discussion:

Resolving patch:
diff --git a/modin/core/dataframe/pandas/partitioning/axis_partition.py b/modin/core/dataframe/pandas/partitioning/axis_partition.py
--- a/modin/core/dataframe/pandas/partitioning/axis_partition.py
+++ b/modin/core/dataframe/pandas/partitioning/axis_partition.py
@@ -135,7 +135,11 @@ def deploy_axis_func(
         # args into kwargs. This is a bit of a hack, but it works.
         result = func(dataframe, *kwargs.pop("args", ()), **kwargs)
 
-        if manual_partition:
+        if num_splits == 1:
+            # If we're not going to split the result, we don't need to specify
+            # split lengths.
+            lengths = None
+        elif manual_partition:
             # The split function is expecting a list
             lengths = list(lengths)
         # We set lengths to None so we don't use the old lengths for the resulting partition
diff --git a/modin/core/storage_formats/pandas/utils.py b/modin/core/storage_formats/pandas/utils.py
--- a/modin/core/storage_formats/pandas/utils.py
+++ b/modin/core/storage_formats/pandas/utils.py
@@ -87,6 +87,9 @@ def split_result_of_axis_func_pandas(axis, num_splits, result, length_list=None)
     list of pandas.DataFrames
         Splitted dataframe represented by list of frames.
     """
+    if num_splits == 1:
+        return [result]
+
     if length_list is not None:
         length_list.insert(0, 0)
         sums = np.cumsum(length_list)
@@ -95,8 +98,6 @@ def split_result_of_axis_func_pandas(axis, num_splits, result, length_list=None)
         else:
             return [result.iloc[:, sums[i] : sums[i + 1]] for i in range(len(sums) - 1)]
 
-    if num_splits == 1:
-        return [result]
     # We do this to restore block partitioning
     chunksize = compute_chunksize(result.shape[axis], num_splits)
     if axis == 0 or isinstance(result, pandas.Series):


Test patch:
diff --git a/modin/test/storage_formats/pandas/test_internals.py b/modin/test/storage_formats/pandas/test_internals.py
--- a/modin/test/storage_formats/pandas/test_internals.py
+++ b/modin/test/storage_formats/pandas/test_internals.py
@@ -367,3 +367,25 @@ def test_virtual_partition_call_queues_at_three_levels(
             level_three_virtual.to_pandas(),
             pd.DataFrame([1, 2, 3, 4], index=[0, 0, 0, 0]),
         )
+
+
+@pytest.mark.skipif(
+    Engine.get() not in ("Dask", "Ray"),
+    reason="Only Dask and Ray engines have virtual partitions.",
+)
+@pytest.mark.parametrize(
+    "virtual_partition_class",
+    (virtual_column_partition_class, virtual_row_partition_class),
+    ids=["partitions_spanning_all_columns", "partitions_spanning_all_rows"],
+)
+def test_virtual_partition_apply_not_returning_pandas_dataframe(
+    virtual_partition_class,
+):
+    # see https://github.com/modin-project/modin/issues/4811
+
+    partition = virtual_partition_class(
+        block_partition_class(put(pandas.DataFrame())), full_axis=False
+    )
+
+    apply_result = partition.apply(lambda df: 1).get()
+    assert apply_result == 1