Repository: modin-project/modin

Issue:
.loc[ lambda, 'col'] = x  got error `TypeError: 'function' object is not iterable` 
### System information
- **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: windows 10
- **Modin version** (`modin.__version__`): 0.10.0
- **Python version**: 3.8.8
- **Code we can use to reproduce**:

Test code:
```
df = pd.DataFrame([[1, 2], [4, 5], [7, 8]],
     index=[7, 8, 9], columns=['max_speed', 'shield'])
df.loc[lambda df: df['shield'] == 8, 'max_speed'] = 0
```

this is valid in pandas.


got error :
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-101-f9afffa1da7a> in <module>
----> 1 df.loc[lambda df: df['shield'] == 8, 'max_speed'] = 0

~\anaconda3\lib\site-packages\modin\pandas\indexing.py in __setitem__(self, key, item)
    594             self.qc = self.df._query_compiler
    595         else:
--> 596             row_lookup, col_lookup = self._compute_lookup(row_loc, col_loc)
    597             super(_LocIndexer, self).__setitem__(
    598                 row_lookup,

~\anaconda3\lib\site-packages\modin\pandas\indexing.py in _compute_lookup(self, row_loc, col_loc)
    675             row_lookup = [i for i, row_val in enumerate(row_loc) if row_val]
    676         else:
--> 677             row_lookup = self.qc.index.get_indexer_for(row_loc)
    678         if isinstance(col_loc, slice):
    679             col_lookup = self.qc.columns.get_indexer_for(

~\anaconda3\lib\site-packages\pandas\core\indexes\base.py in get_indexer_for(self, target, **kwargs)
   4958         """
   4959         if self._index_as_unique:
-> 4960             return self.get_indexer(target, **kwargs)
   4961         indexer, _ = self.get_indexer_non_unique(target)
   4962         return indexer

~\anaconda3\lib\site-packages\pandas\core\indexes\base.py in get_indexer(self, target, method, limit, tolerance)
   3145     ) -> np.ndarray:
   3146         method = missing.clean_reindex_fill_method(method)
-> 3147         target = ensure_index(target)
   3148         if tolerance is not None:
   3149             tolerance = self._convert_tolerance(tolerance, target)

~\anaconda3\lib\site-packages\pandas\core\indexes\base.py in ensure_index(index_like, copy)
   5915             index_like = copy_func(index_like)
   5916 
-> 5917     return Index(index_like)
   5918 
   5919 

~\anaconda3\lib\site-packages\pandas\core\indexes\base.py in __new__(cls, data, dtype, copy, name, tupleize_cols, **kwargs)
    388                     )
    389             # other iterable of some kind
--> 390             subarr = com.asarray_tuplesafe(data, dtype=object)
    391             return Index(subarr, dtype=dtype, copy=copy, name=name, **kwargs)
    392 

~\anaconda3\lib\site-packages\pandas\core\common.py in asarray_tuplesafe(values, dtype)
    199 
    200     if not (isinstance(values, (list, tuple)) or hasattr(values, "__array__")):
--> 201         values = list(values)
    202     elif isinstance(values, ABCIndexClass):
    203         return values._values

TypeError: 'function' object is not iterable

```



Discussion:

Resolving patch:
diff --git a/modin/pandas/indexing.py b/modin/pandas/indexing.py
--- a/modin/pandas/indexing.py
+++ b/modin/pandas/indexing.py
@@ -188,52 +188,6 @@ def is_range_like(obj):
 """
 
 
-def _parse_tuple(tup):
-    """
-    Unpack the user input for getitem and setitem and compute ndim.
-
-    loc[a] -> ([a], :), 1D
-    loc[[a,b],] -> ([a,b], :),
-    loc[a,b] -> ([a], [b]), 0D
-
-    Parameters
-    ----------
-    tup : tuple
-        User input to unpack.
-
-    Returns
-    -------
-    row_loc : list
-        List of row locators.
-    col_list : list
-        List of column locators.
-    ndim : {0, 1, 2}
-        Number of dimensions of located dataset.
-    row_scaler : bool
-        True if `row_loc` is a scalar, False otherwise.
-    col_scaler : bool
-        True if `col_loc` is a scalar, False otherwise.
-    """
-    row_loc, col_loc = slice(None), slice(None)
-
-    if is_tuple(tup):
-        row_loc = tup[0]
-        if len(tup) == 2:
-            col_loc = tup[1]
-        if len(tup) > 2:
-            raise IndexingError("Too many indexers")
-    else:
-        row_loc = tup
-
-    ndim = _compute_ndim(row_loc, col_loc)
-    row_scaler = is_scalar(row_loc)
-    col_scaler = is_scalar(col_loc)
-    row_loc = [row_loc] if row_scaler else row_loc
-    col_loc = [col_loc] if col_scaler else col_loc
-
-    return row_loc, col_loc, ndim, row_scaler, col_scaler
-
-
 def _compute_ndim(row_loc, col_loc):
     """
     Compute the number of dimensions of result from locators.
@@ -497,6 +451,53 @@ def get_axis(axis):
             axis = None
         return axis
 
+    def _parse_row_and_column_locators(self, tup):
+        """
+        Unpack the user input for getitem and setitem and compute ndim.
+
+        loc[a] -> ([a], :), 1D
+        loc[[a,b],] -> ([a,b], :),
+        loc[a,b] -> ([a], [b]), 0D
+
+        Parameters
+        ----------
+        tup : tuple
+            User input to unpack.
+
+        Returns
+        -------
+        row_loc : list
+            List of row locators.
+        col_list : list
+            List of column locators.
+        ndim : {0, 1, 2}
+            Number of dimensions of located dataset.
+        row_scaler : bool
+            True if `row_loc` is a scalar, False otherwise.
+        col_scaler : bool
+            True if `col_loc` is a scalar, False otherwise.
+        """
+        row_loc, col_loc = slice(None), slice(None)
+
+        if is_tuple(tup):
+            row_loc = tup[0]
+            if len(tup) == 2:
+                col_loc = tup[1]
+            if len(tup) > 2:
+                raise IndexingError("Too many indexers")
+        else:
+            row_loc = tup
+
+        row_loc = row_loc(self.df) if callable(row_loc) else row_loc
+        col_loc = col_loc(self.df) if callable(col_loc) else col_loc
+        ndim = _compute_ndim(row_loc, col_loc)
+        row_scaler = is_scalar(row_loc)
+        col_scaler = is_scalar(col_loc)
+        row_loc = [row_loc] if row_scaler else row_loc
+        col_loc = [col_loc] if col_scaler else col_loc
+
+        return row_loc, col_loc, ndim, row_scaler, col_scaler
+
 
 class _LocIndexer(_LocationIndexerBase):
     """
@@ -514,7 +515,7 @@ def __getitem__(self, key):
 
         Parameters
         ----------
-        key : callable or tuple
+        key : callable, scalar, or tuple
             The global row index to retrieve data from.
 
         Returns
@@ -526,9 +527,13 @@ def __getitem__(self, key):
         --------
         pandas.DataFrame.loc
         """
-        if callable(key):
-            return self.__getitem__(key(self.df))
-        row_loc, col_loc, ndim, self.row_scaler, self.col_scaler = _parse_tuple(key)
+        (
+            row_loc,
+            col_loc,
+            ndim,
+            self.row_scaler,
+            self.col_scaler,
+        ) = self._parse_row_and_column_locators(key)
         if isinstance(row_loc, slice) and row_loc == slice(None):
             # If we're only slicing columns, handle the case with `__getitem__`
             if not isinstance(col_loc, slice):
@@ -600,7 +605,13 @@ def __setitem__(self, key, item):
         --------
         pandas.DataFrame.loc
         """
-        row_loc, col_loc, _, row_scaler, col_scaler = _parse_tuple(key)
+        (
+            row_loc,
+            col_loc,
+            _,
+            row_scaler,
+            col_scaler,
+        ) = self._parse_row_and_column_locators(key)
         if isinstance(row_loc, list) and len(row_loc) == 1:
             if row_loc[0] not in self.qc.index:
                 index = self.qc.index.insert(len(self.qc.index), row_loc[0])
@@ -744,9 +755,13 @@ def __getitem__(self, key):
         --------
         pandas.DataFrame.iloc
         """
-        if callable(key):
-            return self.__getitem__(key(self.df))
-        row_loc, col_loc, ndim, self.row_scaler, self.col_scaler = _parse_tuple(key)
+        (
+            row_loc,
+            col_loc,
+            ndim,
+            self.row_scaler,
+            self.col_scaler,
+        ) = self._parse_row_and_column_locators(key)
         self._check_dtypes(row_loc)
         self._check_dtypes(col_loc)
 
@@ -772,7 +787,13 @@ def __setitem__(self, key, item):
         --------
         pandas.DataFrame.iloc
         """
-        row_loc, col_loc, _, row_scaler, col_scaler = _parse_tuple(key)
+        (
+            row_loc,
+            col_loc,
+            _,
+            row_scaler,
+            col_scaler,
+        ) = self._parse_row_and_column_locators(key)
         self._check_dtypes(row_loc)
         self._check_dtypes(col_loc)
 


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
@@ -223,6 +223,12 @@ def test_iloc(request, data):
             modin_df.iloc[lambda df: df.index.get_indexer_for(df.index[:5])],
             pandas_df.iloc[lambda df: df.index.get_indexer_for(df.index[:5])],
         )
+
+        # Read values, selecting rows with callable and a column with a scalar.
+        df_equals(
+            pandas_df.iloc[lambda df: df.index.get_indexer_for(df.index[:5]), 0],
+            modin_df.iloc[lambda df: df.index.get_indexer_for(df.index[:5]), 0],
+        )
     else:
         with pytest.raises(IndexError):
             modin_df.iloc[0, 1]
@@ -323,12 +329,32 @@ def test_loc(data):
     pandas_df_copy.loc[[1, 2]] = 42
     df_equals(modin_df_copy, pandas_df_copy)
 
+    # Write an item, selecting rows with a callable.
+    modin_df_copy2 = modin_df.copy()
+    pandas_df_copy2 = pandas_df.copy()
+    modin_df_copy2.loc[lambda df: df[key1].isin(list(range(1000)))] = 42
+    pandas_df_copy2.loc[lambda df: df[key1].isin(list(range(1000)))] = 42
+    df_equals(modin_df_copy2, pandas_df_copy2)
+
+    # Write an item, selecting rows with a callable and a column with a scalar.
+    modin_df_copy3 = modin_df.copy()
+    pandas_df_copy3 = pandas_df.copy()
+    modin_df_copy3.loc[lambda df: df[key1].isin(list(range(1000))), key1] = 42
+    pandas_df_copy3.loc[lambda df: df[key1].isin(list(range(1000))), key1] = 42
+    df_equals(modin_df_copy3, pandas_df_copy3)
+
     # From issue #1775
     df_equals(
         modin_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
         pandas_df.loc[lambda df: df.iloc[:, 0].isin(list(range(1000)))],
     )
 
+    # Read values, selecting rows with a callable and a column with a scalar.
+    df_equals(
+        pandas_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
+        modin_df.loc[lambda df: df[key1].isin(list(range(1000))), key1],
+    )
+
     # From issue #1374
     with pytest.raises(KeyError):
         modin_df.loc["NO_EXIST"]
@@ -485,15 +511,16 @@ def test_iloc_assignment():
     modin_df.iloc[0]["col1"] = 11
     modin_df.iloc[1]["col1"] = 21
     modin_df.iloc[2]["col1"] = 31
-    modin_df.iloc[0]["col2"] = 12
-    modin_df.iloc[1]["col2"] = 22
-    modin_df.iloc[2]["col2"] = 32
+    modin_df.iloc[lambda df: 0]["col2"] = 12
+    modin_df.iloc[1][lambda df: ["col2"]] = 22
+    modin_df.iloc[lambda df: 2][lambda df: ["col2"]] = 32
     pandas_df.iloc[0]["col1"] = 11
     pandas_df.iloc[1]["col1"] = 21
     pandas_df.iloc[2]["col1"] = 31
-    pandas_df.iloc[0]["col2"] = 12
-    pandas_df.iloc[1]["col2"] = 22
-    pandas_df.iloc[2]["col2"] = 32
+    pandas_df.iloc[lambda df: 0]["col2"] = 12
+    pandas_df.iloc[1][lambda df: ["col2"]] = 22
+    pandas_df.iloc[lambda df: 2][lambda df: ["col2"]] = 32
+
     df_equals(modin_df, pandas_df)