|
14 | 14 | import numpy as np
|
15 | 15 |
|
16 | 16 | from pandas._libs.indexing import NDFrameIndexerBase
|
17 |
| -from pandas._libs.lib import item_from_zerodim |
| 17 | +from pandas._libs.lib import ( |
| 18 | + is_np_dtype, |
| 19 | + item_from_zerodim, |
| 20 | +) |
18 | 21 | from pandas.compat import PYPY
|
19 | 22 | from pandas.compat._constants import (
|
20 | 23 | REF_COUNT,
|
|
35 | 38 |
|
36 | 39 | from pandas.core.dtypes.cast import (
|
37 | 40 | can_hold_element,
|
38 |
| - maybe_promote, |
| 41 | + maybe_downcast_to_dtype, |
39 | 42 | )
|
40 | 43 | from pandas.core.dtypes.common import (
|
41 | 44 | is_array_like,
|
|
50 | 53 | is_sequence,
|
51 | 54 | )
|
52 | 55 | from pandas.core.dtypes.concat import concat_compat
|
53 |
| -from pandas.core.dtypes.dtypes import ExtensionDtype |
| 56 | +from pandas.core.dtypes.dtypes import ( |
| 57 | + ExtensionDtype, |
| 58 | + NumpyEADtype, |
| 59 | +) |
54 | 60 | from pandas.core.dtypes.generic import (
|
55 | 61 | ABCDataFrame,
|
56 | 62 | ABCSeries,
|
|
59 | 65 | construct_1d_array_from_inferred_fill_value,
|
60 | 66 | infer_fill_value,
|
61 | 67 | is_valid_na_for_dtype,
|
62 |
| - isna, |
63 | 68 | na_value_for_dtype,
|
64 | 69 | )
|
65 | 70 |
|
|
87 | 92 | )
|
88 | 93 |
|
89 | 94 | from pandas._typing import (
|
| 95 | + ArrayLike, |
90 | 96 | Axis,
|
91 | 97 | AxisInt,
|
92 | 98 | T,
|
|
97 | 103 | DataFrame,
|
98 | 104 | Series,
|
99 | 105 | )
|
| 106 | + from pandas.core.arrays import ExtensionArray |
100 | 107 |
|
101 | 108 | # "null slice"
|
102 | 109 | _NS = slice(None, None)
|
@@ -934,14 +941,55 @@ def __setitem__(self, key, value) -> None:
|
934 | 941 | else:
|
935 | 942 | maybe_callable = com.apply_if_callable(key, self.obj)
|
936 | 943 | key = self._raise_callable_usage(key, maybe_callable)
|
937 |
| - indexer = self._get_setitem_indexer(key) |
| 944 | + orig_obj = self.obj[:].iloc[:0].copy() # copy to avoid extra refs |
| 945 | + indexer = self._get_setitem_indexer(key) # may alter self.obj |
938 | 946 | self._has_valid_setitem_indexer(key)
|
939 | 947 |
|
940 | 948 | iloc: _iLocIndexer = (
|
941 | 949 | cast("_iLocIndexer", self) if self.name == "iloc" else self.obj.iloc
|
942 | 950 | )
|
943 | 951 | iloc._setitem_with_indexer(indexer, value, self.name)
|
944 | 952 |
|
| 953 | + self._post_expansion_casting(orig_obj) |
| 954 | + |
| 955 | + def _post_expansion_casting(self, orig_obj) -> None: |
| 956 | + if orig_obj.shape[0] != self.obj.shape[0]: |
| 957 | + # setitem-with-expansion added new rows. Try to retain |
| 958 | + # original dtypes |
| 959 | + if orig_obj.ndim == 1: |
| 960 | + if orig_obj.dtype != self.obj.dtype: |
| 961 | + new_arr = infer_and_maybe_downcast(orig_obj.array, self.obj._values) |
| 962 | + new_ser = self.obj._constructor( |
| 963 | + new_arr, index=self.obj.index, name=self.obj.name |
| 964 | + ) |
| 965 | + self.obj._mgr = new_ser._mgr |
| 966 | + elif orig_obj.shape[1] == self.obj.shape[1]: |
| 967 | + # We added rows but not columns |
| 968 | + for i in range(orig_obj.shape[1]): |
| 969 | + new_dtype = self.obj.dtypes.iloc[i] |
| 970 | + orig_dtype = orig_obj.dtypes.iloc[i] |
| 971 | + if new_dtype != orig_dtype: |
| 972 | + new_arr = infer_and_maybe_downcast( |
| 973 | + orig_obj.iloc[:, i].array, self.obj.iloc[:, i]._values |
| 974 | + ) |
| 975 | + self.obj.isetitem(i, new_arr) |
| 976 | + |
| 977 | + elif orig_obj.columns.is_unique and self.obj.columns.is_unique: |
| 978 | + for col in orig_obj.columns: |
| 979 | + new_dtype = self.obj[col].dtype |
| 980 | + orig_dtype = orig_obj[col].dtype |
| 981 | + if new_dtype != orig_dtype: |
| 982 | + new_arr = infer_and_maybe_downcast( |
| 983 | + orig_obj[col].array, self.obj[col]._values |
| 984 | + ) |
| 985 | + self.obj[col] = new_arr |
| 986 | + else: |
| 987 | + # In these cases there isn't a one-to-one correspondence between |
| 988 | + # old columns and new columns, which makes casting hairy. |
| 989 | + # Punt on these for now, as there are no tests that get here |
| 990 | + # as of 2025-09-29 |
| 991 | + pass |
| 992 | + |
945 | 993 | def _validate_key(self, key, axis: AxisInt) -> None:
|
946 | 994 | """
|
947 | 995 | Ensure that key is valid for current indexer.
|
@@ -2189,9 +2237,10 @@ def _setitem_single_column(self, loc: int, value, plane_indexer) -> None:
|
2189 | 2237 | # Columns F and G will initially be set to np.void.
|
2190 | 2238 | # Here, we replace those temporary `np.void` columns with
|
2191 | 2239 | # columns of the appropriate dtype, based on `value`.
|
2192 |
| - self.obj.iloc[:, loc] = construct_1d_array_from_inferred_fill_value( |
| 2240 | + new_arr = construct_1d_array_from_inferred_fill_value( |
2193 | 2241 | value, len(self.obj)
|
2194 | 2242 | )
|
| 2243 | + self.obj.isetitem(loc, new_arr) |
2195 | 2244 | self.obj._mgr.column_setitem(loc, plane_indexer, value)
|
2196 | 2245 |
|
2197 | 2246 | def _setitem_single_block(self, indexer, value, name: str) -> None:
|
@@ -2260,27 +2309,14 @@ def _setitem_with_indexer_missing(self, indexer, value):
|
2260 | 2309 |
|
2261 | 2310 | # this preserves dtype of the value and of the object
|
2262 | 2311 | if not is_scalar(value):
|
2263 |
| - new_dtype = None |
| 2312 | + pass |
2264 | 2313 |
|
2265 | 2314 | elif is_valid_na_for_dtype(value, self.obj.dtype):
|
2266 | 2315 | if not is_object_dtype(self.obj.dtype):
|
2267 | 2316 | # Every NA value is suitable for object, no conversion needed
|
2268 | 2317 | value = na_value_for_dtype(self.obj.dtype, compat=False)
|
2269 | 2318 |
|
2270 |
| - new_dtype = maybe_promote(self.obj.dtype, value)[0] |
2271 |
| - |
2272 |
| - elif isna(value): |
2273 |
| - new_dtype = None |
2274 |
| - elif not self.obj.empty and not is_object_dtype(self.obj.dtype): |
2275 |
| - # We should not cast, if we have object dtype because we can |
2276 |
| - # set timedeltas into object series |
2277 |
| - curr_dtype = self.obj.dtype |
2278 |
| - curr_dtype = getattr(curr_dtype, "numpy_dtype", curr_dtype) |
2279 |
| - new_dtype = maybe_promote(curr_dtype, value)[0] |
2280 |
| - else: |
2281 |
| - new_dtype = None |
2282 |
| - |
2283 |
| - new_values = Series([value], dtype=new_dtype)._values |
| 2319 | + new_values = infer_and_maybe_downcast(self.obj.array, [value]) |
2284 | 2320 |
|
2285 | 2321 | if len(self.obj._values):
|
2286 | 2322 | # GH#22717 handle casting compatibility that np.concatenate
|
@@ -2808,3 +2844,15 @@ def check_dict_or_set_indexers(key) -> None:
|
2808 | 2844 | raise TypeError(
|
2809 | 2845 | "Passing a dict as an indexer is not supported. Use a list instead."
|
2810 | 2846 | )
|
| 2847 | + |
| 2848 | + |
| 2849 | +def infer_and_maybe_downcast(orig: ExtensionArray, new_arr) -> ArrayLike: |
| 2850 | + new_arr = orig._cast_pointwise_result(new_arr) |
| 2851 | + |
| 2852 | + dtype = orig.dtype |
| 2853 | + if isinstance(dtype, NumpyEADtype): |
| 2854 | + dtype = dtype.numpy_dtype |
| 2855 | + |
| 2856 | + if is_np_dtype(new_arr.dtype, "f") and is_np_dtype(dtype, "iu"): |
| 2857 | + new_arr = maybe_downcast_to_dtype(new_arr, dtype) |
| 2858 | + return new_arr |
0 commit comments