diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index a0e3168f3bc6f..1ea06e5473805 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -1813,21 +1813,10 @@ def _get_setitem_indexer(self, key): # ------------------------------------------------------------------- - def _setitem_with_indexer(self, indexer, value, name: str = "iloc") -> None: + def _decide_split_path(self, indexer, value) -> bool: """ - _setitem_with_indexer is for setting values on a Series/DataFrame - using positional indexers. - - If the relevant keys are not present, the Series/DataFrame may be - expanded. - - This method is currently broken when dealing with non-unique Indexes, - since it goes from positional indexers back to labels when calling - BlockManager methods, see GH#12991, GH#22046, GH#15686. + Decide whether we will take a block-by-block path. """ - info_axis = self.obj._info_axis_number - - # maybe partial set take_split_path = not self.obj._mgr.is_single_block if not take_split_path and isinstance(value, ABCDataFrame): @@ -1855,77 +1844,88 @@ def _setitem_with_indexer(self, indexer, value, name: str = "iloc") -> None: take_split_path = True break + return take_split_path + + def _setitem_new_column(self, indexer, key, value, name: str) -> None: + """ + _setitem_with_indexer cases that can go through DataFrame.__setitem__. + """ + # add the new item, and set the value + # must have all defined axes if we have a scalar + # or a list-like on the non-info axes if we have a + # list-like + if not len(self.obj): + if not is_list_like_indexer(value): + raise ValueError( + "cannot set a frame with no defined index and a scalar" + ) + self.obj[key] = value + return + + # add a new item with the dtype setup + if com.is_null_slice(indexer[0]): + # We are setting an entire column + self.obj[key] = value + return + elif is_array_like(value): + # GH#42099 + arr = extract_array(value, extract_numpy=True) + taker = -1 * np.ones(len(self.obj), dtype=np.intp) + empty_value = algos.take_nd(arr, taker) + if not isinstance(value, ABCSeries): + # if not Series (in which case we need to align), + # we can short-circuit + if isinstance(arr, np.ndarray) and arr.ndim == 1 and len(arr) == 1: + # NumPy 1.25 deprecation: https://github.com/numpy/numpy/pull/10615 + arr = arr[0, ...] + empty_value[indexer[0]] = arr + self.obj[key] = empty_value + return + + self.obj[key] = empty_value + elif not is_list_like(value): + self.obj[key] = construct_1d_array_from_inferred_fill_value( + value, len(self.obj) + ) + else: + # FIXME: GH#42099#issuecomment-864326014 + self.obj[key] = infer_fill_value(value) + + new_indexer = convert_from_missing_indexer_tuple(indexer, self.obj.axes) + self._setitem_with_indexer(new_indexer, value, name) + + return + + def _setitem_with_indexer(self, indexer, value, name: str = "iloc") -> None: + """ + _setitem_with_indexer is for setting values on a Series/DataFrame + using positional indexers. + + If the relevant keys are not present, the Series/DataFrame may be + expanded. + """ + info_axis = self.obj._info_axis_number + take_split_path = self._decide_split_path(indexer, value) + if isinstance(indexer, tuple): nindexer = [] for i, idx in enumerate(indexer): - if isinstance(idx, dict): + idx, missing = convert_missing_indexer(idx) + if missing: # reindex the axis to the new value # and set inplace - key, _ = convert_missing_indexer(idx) + key = idx # if this is the items axes, then take the main missing # path first - # this correctly sets the dtype and avoids cache issues + # this correctly sets the dtype # essentially this separates out the block that is needed # to possibly be modified if self.ndim > 1 and i == info_axis: - # add the new item, and set the value - # must have all defined axes if we have a scalar - # or a list-like on the non-info axes if we have a - # list-like - if not len(self.obj): - if not is_list_like_indexer(value): - raise ValueError( - "cannot set a frame with no " - "defined index and a scalar" - ) - self.obj[key] = value - return - - # add a new item with the dtype setup - if com.is_null_slice(indexer[0]): - # We are setting an entire column - self.obj[key] = value - return - elif is_array_like(value): - # GH#42099 - arr = extract_array(value, extract_numpy=True) - taker = -1 * np.ones(len(self.obj), dtype=np.intp) - empty_value = algos.take_nd(arr, taker) - if not isinstance(value, ABCSeries): - # if not Series (in which case we need to align), - # we can short-circuit - if ( - isinstance(arr, np.ndarray) - and arr.ndim == 1 - and len(arr) == 1 - ): - # NumPy 1.25 deprecation: https://github.com/numpy/numpy/pull/10615 - arr = arr[0, ...] - empty_value[indexer[0]] = arr - self.obj[key] = empty_value - return - - self.obj[key] = empty_value - elif not is_list_like(value): - self.obj[key] = construct_1d_array_from_inferred_fill_value( - value, len(self.obj) - ) - else: - # FIXME: GH#42099#issuecomment-864326014 - self.obj[key] = infer_fill_value(value) - - new_indexer = convert_from_missing_indexer_tuple( - indexer, self.obj.axes - ) - self._setitem_with_indexer(new_indexer, value, name) - + self._setitem_new_column(indexer, key, value, name=name) return # reindex the axis - # make sure to clear the cache because we are - # just replacing the block manager here - # so the object is the same index = self.obj._get_axis(i) labels = index.insert(len(index), key) @@ -2722,7 +2722,7 @@ def convert_missing_indexer(indexer): return indexer, False -def convert_from_missing_indexer_tuple(indexer, axes): +def convert_from_missing_indexer_tuple(indexer: tuple, axes: list[Index]) -> tuple: """ Create a filtered indexer that doesn't have any missing indexers. """ diff --git a/pandas/util/_decorators.py b/pandas/util/_decorators.py index 6a73615534b22..dd2ed6c00e48c 100644 --- a/pandas/util/_decorators.py +++ b/pandas/util/_decorators.py @@ -177,6 +177,8 @@ def deprecate_kwarg( def _deprecate_kwarg(func: F) -> F: @wraps(func) def wrapper(*args, **kwargs) -> Callable[..., Any]: + __tracebackhide__ = True + old_arg_value = kwargs.pop(old_arg_name, None) if old_arg_value is not None: