Repository: modin-project/modin (python)

Issue:
BUG: series assignment raises a `TypeError`
### 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.

- [ ] 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-main-branch).)


### Reproducible Example

```python
import modin.pandas as mpd
series = mpd.Series([1,2,3], name="a")
values = mpd.Series([100, 200], name="b")
series.iloc[[0, 2]] = values

> TypeError: got an unexpected keyword argument 'copy'
```

### Issue Description

Both pandas and modin 0.32 do not raise any exception and allow this behavior.

Spotted in Narwhals together with other `iloc/loc` issues when used to assigning new values



### Expected Behavior

Match pandas behavior

### Error Logs

<details>

```python-traceback

TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 series.iloc[[0, 2]] = values

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/core/storage_formats/pandas/query_compiler_caster.py:1100, in wrap_function_in_argument_caster.<locals>.f_with_argument_casting(*args, **kwargs)
   1097 # We have to set the global Backend correctly for I/O methods like
   1098 # read_json() to use the correct backend.
   1099 with config_context(Backend=result_backend):
-> 1100     result = f_to_apply(*args, **kwargs)
   1101 for (
   1102     original_castable,
   1103     original_qc,
   1104     new_castable,
   1105 ) in inplace_update_trackers:
   1106     new_qc = new_castable._get_query_compiler()

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/logging/logger_decorator.py:149, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    147 start_time = perf_counter()
    148 if LogMode.get() == "disable":
--> 149     result = obj(*args, **kwargs)
    150     emit_metric(metric_name, perf_counter() - start_time)
    151     return result

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/pandas/indexing.py:1163, in _iLocIndexer.__setitem__(self, key, item)
   1160 self._check_dtypes(col_loc)
   1162 row_lookup, col_lookup = self._compute_lookup(row_loc, col_loc)
-> 1163 self._setitem_positional(
   1164     row_lookup,
   1165     col_lookup,
   1166     item,
   1167     axis=self._determine_setitem_axis(
   1168         row_lookup, col_lookup, row_scalar, col_scalar
   1169     ),
   1170 )

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/core/storage_formats/pandas/query_compiler_caster.py:1100, in wrap_function_in_argument_caster.<locals>.f_with_argument_casting(*args, **kwargs)
   1097 # We have to set the global Backend correctly for I/O methods like
   1098 # read_json() to use the correct backend.
   1099 with config_context(Backend=result_backend):
-> 1100     result = f_to_apply(*args, **kwargs)
   1101 for (
   1102     original_castable,
   1103     original_qc,
   1104     new_castable,
   1105 ) in inplace_update_trackers:
   1106     new_qc = new_castable._get_query_compiler()

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/core/storage_formats/pandas/query_compiler_caster.py:1100, in wrap_function_in_argument_caster.<locals>.f_with_argument_casting(*args, **kwargs)
   1097 # We have to set the global Backend correctly for I/O methods like
   1098 # read_json() to use the correct backend.
   1099 with config_context(Backend=result_backend):
-> 1100     result = f_to_apply(*args, **kwargs)
   1101 for (
   1102     original_castable,
   1103     original_qc,
   1104     new_castable,
   1105 ) in inplace_update_trackers:
   1106     new_qc = new_castable._get_query_compiler()

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/logging/logger_decorator.py:149, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    147 start_time = perf_counter()
    148 if LogMode.get() == "disable":
--> 149     result = obj(*args, **kwargs)
    150     emit_metric(metric_name, perf_counter() - start_time)
    151     return result

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/pandas/indexing.py:527, in _LocationIndexerBase._setitem_positional(self, row_lookup, col_lookup, item, axis)
    524     self.df._create_or_update_from_compiler(new_qc, inplace=True)
    525 # Assignment to both axes.
    526 else:
--> 527     new_qc = self.qc.write_items(row_lookup, col_lookup, item)
    528     self.df._create_or_update_from_compiler(new_qc, inplace=True)
    530 self.qc = self.df._query_compiler

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/logging/logger_decorator.py:149, in enable_logging.<locals>.decorator.<locals>.run_and_log(*args, **kwargs)
    147 start_time = perf_counter()
    148 if LogMode.get() == "disable":
--> 149     result = obj(*args, **kwargs)
    150     emit_metric(metric_name, perf_counter() - start_time)
    151     return result

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/core/storage_formats/pandas/query_compiler.py:4711, in PandasQueryCompiler.write_items(self, row_numeric_index, col_numeric_index, item, need_columns_reindex)
   4703     return partition
   4705 if not is_scalar(item):
   4706     (
   4707         broadcasted_item,
   4708         broadcasted_dtypes,
   4709         row_numeric_index,
   4710         col_numeric_index,
-> 4711     ) = broadcast_item(
   4712         self,
   4713         row_numeric_index,
   4714         col_numeric_index,
   4715         item,
   4716         need_columns_reindex=need_columns_reindex,
   4717     )
   4718 else:
   4719     broadcasted_item, broadcasted_dtypes = item, pandas.Series(
   4720         [extract_dtype(item)] * len(col_numeric_index)
   4721     )

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/pandas/utils.py:348, in broadcast_item(obj, row_lookup, col_lookup, item, need_columns_reindex, sort_lookups_and_item)
    343         dtypes = pandas.Series([dtypes])
    345 try:
    346     # Cast to numpy drop information about heterogeneous types (cast to common)
    347     # TODO: we shouldn't do that, maybe there should be the if branch
--> 348     item = np.array(item)
    350     def sort_index(lookup: Any) -> np.ndarray:
    351         """
    352         Return the argsort and sorted version of the lookup index.
    353 
   (...)    357         will use the last item encountered if two items share an index.
    358         """

File ~/open-source/narwhals/.venv/lib/python3.12/site-packages/modin/core/storage_formats/pandas/query_compiler_caster.py:1030, in wrap_function_in_argument_caster.<locals>.f_with_argument_casting(*args, **kwargs)
   1025     return result
   1027 # Bind the arguments using the function implementation for the input
   1028 # backend. TODO(https://github.com/modin-project/modin/issues/7525):
   1029 # Ideally every implementation would have the same signature.
-> 1030 bound_arguments = inspect.signature(
   1031     _get_extension_for_method(
   1032         name=name,
   1033         extensions=extensions,
   1034         backend=input_backend,
   1035         args=args,
   1036         wrapping_function_type=wrapping_function_type,
   1037     ),
   1038 ).bind(*args, **kwargs)
   1039 bound_arguments.apply_defaults()
   1040 args_dict = MappingProxyType(bound_arguments.arguments)

File /usr/lib/python3.12/inspect.py:3277, in Signature.bind(self, *args, **kwargs)
   3272 def bind(self, /, *args, **kwargs):
   3273     """Get a BoundArguments object, that maps the passed `args`
   3274     and `kwargs` to the function's signature.  Raises `TypeError`
   3275     if the passed arguments can not be bound.
   3276     """
-> 3277     return self._bind(args, kwargs)

File /usr/lib/python3.12/inspect.py:3266, in Signature._bind(self, args, kwargs, partial)
   3256         raise TypeError(
   3257             'got some positional-only arguments passed as '
   3258             'keyword arguments: {arg!r}'.format(
   (...)   3263             ),
   3264         )
   3265     else:
-> 3266         raise TypeError(
   3267             'got an unexpected keyword argument {arg!r}'.format(
   3268                 arg=next(iter(kwargs))))
   3270 return self._bound_arguments_cls(self, arguments)

TypeError: got an unexpected keyword argument 'copy'
```

</details>


### Installed Versions

<details>

pandas                : 2.2.3
modin                 : 0.33.0
numpy                 : 2.2.6

</details>


Pull request description:
FIX-#7582: Add copy parameter to __array__ methods.
The new signature of `__array__` should be backwards-compatible with callers that do not pass the `copy` keyword argument at all.

I tested this change locally with python 3.10 and 3.12. In CI, we test python 3.10 and python 3.11 [periodically](https://github.com/modin-project/modin/blob/cc19143ba2abe7b18e0fbd6efd26c7dcad5e39d6/.github/workflows/ci.yml#L39-L44), as [here](https://github.com/modin-project/modin/actions/runs/15152514188/job/42601094103).

Fixes #7582 

---------------
Signed-off-by: sfc-gh-mvashishtha <mahesh.vashishtha@snowflake.com>

Patch:
diff --git a/modin/pandas/base.py b/modin/pandas/base.py
index ae534240..73770f28 100644
--- a/modin/pandas/base.py
+++ b/modin/pandas/base.py
@@ -3884,7 +3884,9 @@ class BasePandasDataset(QueryCompilerCaster, ClassLogger):
     def __rand__(self, other) -> Self:
         return self._binary_op("__rand__", other, axis=0)
 
-    def __array__(self, dtype=None) -> np.ndarray:
+    def __array__(
+        self, dtype: npt.DTypeLike | None = None, copy: bool | None = None
+    ) -> np.ndarray:
         """
         Return the values as a NumPy array.
 
@@ -3892,6 +3894,9 @@ class BasePandasDataset(QueryCompilerCaster, ClassLogger):
         ----------
         dtype : str or np.dtype, optional
             The dtype of returned array.
+        copy : bool, default: None
+            This parameter has no effect; the method always returns a copy of
+            the data.
 
         Returns
         -------
diff --git a/modin/pandas/series.py b/modin/pandas/series.py
index 94b27c54..313fa256 100644
--- a/modin/pandas/series.py
+++ b/modin/pandas/series.py
@@ -238,10 +238,9 @@ class Series(BasePandasDataset):
 
     # add `_inherit_docstrings` decorator to force method link addition.
     @_inherit_docstrings(pandas.Series.__array__, apilink="pandas.Series.__array__")
-    def __array__(self, dtype=None) -> np.ndarray:  # noqa: PR01, RT01, D200
-        """
-        Return the values as a NumPy array.
-        """
+    def __array__(
+        self, dtype: npt.DTypeLike | None = None, copy: bool | None = None
+    ) -> np.ndarray:
         return super(Series, self).__array__(dtype).flatten()
 
     def __column_consortium_standard__(


Test patch:
diff --git a/modin/tests/pandas/dataframe/test_default.py b/modin/tests/pandas/dataframe/test_default.py
index ebc571e4..10285ca8 100644
--- a/modin/tests/pandas/dataframe/test_default.py
+++ b/modin/tests/pandas/dataframe/test_default.py
@@ -1443,9 +1443,29 @@ def test_unstack_multiindex_types(multi_col, multi_idx):
 
 
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
-def test___array__(data):
-    modin_df, pandas_df = pd.DataFrame(data), pandas.DataFrame(data)
-    assert_array_equal(modin_df.__array__(), pandas_df.__array__())
+@pytest.mark.parametrize("copy_kwargs", ({"copy": True}, {"copy": None}, {}))
+@pytest.mark.parametrize(
+    "get_array",
+    (
+        lambda df, copy_kwargs: df.__array__(**copy_kwargs),
+        lambda df, copy_kwargs: np.array(df, **copy_kwargs),
+    ),
+)
+def test___array__(data, copy_kwargs, get_array):
+    assert_array_equal(*(get_array(df, copy_kwargs) for df in create_test_dfs(data)))
+
+
+@pytest.mark.xfail(
+    raises=AssertionError, reason="https://github.com/modin-project/modin/issues/4650"
+)
+def test___array__copy_false_creates_view():
+    def do_in_place_update_via_copy(df):
+        array = np.array(df, copy=False)
+        array[0, 0] += 1
+
+    eval_general(
+        *create_test_dfs([[11]]), do_in_place_update_via_copy, __inplace__=True
+    )
 
 
 @pytest.mark.parametrize("data", [[False], [True], [1, 2]])
diff --git a/modin/tests/pandas/test_series.py b/modin/tests/pandas/test_series.py
index 8b7a16a2..0af3841b 100644
--- a/modin/tests/pandas/test_series.py
+++ b/modin/tests/pandas/test_series.py
@@ -313,10 +313,29 @@ def test___and__(data, request):
 
 
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)
-def test___array__(data):
-    modin_series, pandas_series = create_test_series(data)
-    modin_result = modin_series.__array__()
-    assert_array_equal(modin_result, pandas_series.__array__())
+@pytest.mark.parametrize("copy_kwargs", ({"copy": True}, {"copy": None}, {}))
+@pytest.mark.parametrize(
+    "get_array",
+    (
+        lambda df, copy_kwargs: df.__array__(**copy_kwargs),
+        lambda df, copy_kwargs: np.array(df, **copy_kwargs),
+    ),
+)
+def test___array__(data, copy_kwargs, get_array):
+    assert_array_equal(*(get_array(df, copy_kwargs) for df in create_test_series(data)))
+
+
+@pytest.mark.xfail(
+    raises=AssertionError, reason="https://github.com/modin-project/modin/issues/4650"
+)
+def test___array__copy_false_creates_view():
+    def do_in_place_update_via_copy(series):
+        array = np.array(series, copy=False)
+        array[0] += 1
+
+    eval_general(
+        *create_test_series([11]), do_in_place_update_via_copy, __inplace__=True
+    )
 
 
 @pytest.mark.parametrize("data", test_data_values, ids=test_data_keys)