Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d66ab22
Test chained assignment detection for Python 3.14
jorisvandenbossche Aug 8, 2025
7d5a732
fix pickling
jorisvandenbossche Aug 8, 2025
ae84326
tweak mro to get it working on python 3.14, fix refcount check for ol…
jorisvandenbossche Aug 8, 2025
29e1a95
add explicit tests for loc/iloc/at
jorisvandenbossche Aug 8, 2025
9646c02
remove warning filter for spss
jorisvandenbossche Aug 8, 2025
622efa5
Merge remote-tracking branch 'upstream/main' into cow-chained-detecti…
jorisvandenbossche Aug 15, 2025
efea93f
cleanup
jorisvandenbossche Aug 15, 2025
1188bf1
Merge remote-tracking branch 'upstream/main' into cow-chained-detecti…
jorisvandenbossche Sep 5, 2025
7d506cb
add back filterwarnings for spss cython issue
jorisvandenbossche Sep 5, 2025
022de21
update internals.pyi
jorisvandenbossche Sep 5, 2025
b875ad2
typing: __setstate__ no longer final
jorisvandenbossche Sep 5, 2025
f4c39a2
Merge remote-tracking branch 'upstream/main' into cow-chained-detecti…
jorisvandenbossche Sep 11, 2025
080ceca
Merge remote-tracking branch 'upstream/main' into cow-chained-detecti…
jorisvandenbossche Oct 9, 2025
923dc27
keep pypy check in cython version
jorisvandenbossche Oct 9, 2025
a1f326d
Merge remote-tracking branch 'upstream/main' into cow-chained-detecti…
jorisvandenbossche Oct 20, 2025
798fcdc
fixup
jorisvandenbossche Oct 21, 2025
de2e68b
differentiate between generic warning and inplace methods -> Py314 st…
jorisvandenbossche Oct 21, 2025
d8214ea
disable warning check on py314t
jorisvandenbossche Oct 21, 2025
818c8d4
suppress mypy about _is_gil_enabled
jorisvandenbossche Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pandas/_libs/internals.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@ class BlockValuesRefs:
def add_reference(self, blk: Block) -> None: ...
def add_index_reference(self, index: Index) -> None: ...
def has_reference(self) -> bool: ...

class SetitemMixin:
def __setitem__(self, key, value) -> None: ...
def __delitem__(self, key) -> None: ...
46 changes: 46 additions & 0 deletions pandas/_libs/internals.pyx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from collections import defaultdict
import sys
import warnings

cimport cython
from cpython cimport PY_VERSION_HEX
from cpython.object cimport PyObject
from cpython.pyport cimport PY_SSIZE_T_MAX
from cpython.slice cimport PySlice_GetIndicesEx
Expand All @@ -20,6 +23,9 @@ from numpy cimport (
cnp.import_array()

from pandas._libs.algos import ensure_int64
from pandas.compat import CHAINED_WARNING_DISABLED
from pandas.errors import ChainedAssignmentError
from pandas.errors.cow import _chained_assignment_msg

from pandas._libs.util cimport (
is_array,
Expand Down Expand Up @@ -996,3 +1002,43 @@ cdef class BlockValuesRefs:
return self._has_reference_maybe_locked()
ELSE:
return self._has_reference_maybe_locked()


cdef extern from "Python.h":
"""
#if PY_VERSION_HEX < 0x030E0000
int __Pyx_PyUnstable_Object_IsUniqueReferencedTemporary(PyObject *ref);
#else
#define __Pyx_PyUnstable_Object_IsUniqueReferencedTemporary \
PyUnstable_Object_IsUniqueReferencedTemporary
#endif
"""
int PyUnstable_Object_IsUniqueReferencedTemporary\
"__Pyx_PyUnstable_Object_IsUniqueReferencedTemporary"(object o) except -1


# Python version compatibility for PyUnstable_Object_IsUniqueReferencedTemporary
cdef inline bint _is_unique_referenced_temporary(object obj) except -1:
if PY_VERSION_HEX >= 0x030E0000:
# Python 3.14+ has PyUnstable_Object_IsUniqueReferencedTemporary
return PyUnstable_Object_IsUniqueReferencedTemporary(obj)
else:
# Fallback for older Python versions using sys.getrefcount
return sys.getrefcount(obj) <= 1


cdef class SetitemMixin:
# class used in DataFrame and Series for checking for chained assignment

def __setitem__(self, key, value) -> None:
cdef bint is_unique = 0
if not CHAINED_WARNING_DISABLED:
is_unique = _is_unique_referenced_temporary(self)
if is_unique:
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=1
)
self._setitem(key, value)

def __delitem__(self, key) -> None:
self._delitem(key)
16 changes: 12 additions & 4 deletions pandas/_testing/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import uuid

from pandas.compat import (
PYPY,
WARNING_CHECK_DISABLED,
CHAINED_WARNING_DISABLED,
CHAINED_WARNING_DISABLED_INPLACE_METHOD,
)
from pandas.errors import ChainedAssignmentError

Expand Down Expand Up @@ -163,10 +163,18 @@ def with_csv_dialect(name: str, **kwargs) -> Generator[None]:
csv.unregister_dialect(name)


def raises_chained_assignment_error(extra_warnings=(), extra_match=()):
def raises_chained_assignment_error(
extra_warnings=(), extra_match=(), inplace_method=False
):
from pandas._testing import assert_produces_warning

if PYPY or WARNING_CHECK_DISABLED:
WARNING_DISABLED = (
CHAINED_WARNING_DISABLED_INPLACE_METHOD
if inplace_method
else CHAINED_WARNING_DISABLED
)

if WARNING_DISABLED:
if not extra_warnings:
from contextlib import nullcontext

Expand Down
6 changes: 4 additions & 2 deletions pandas/compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
from typing import TYPE_CHECKING

from pandas.compat._constants import (
CHAINED_WARNING_DISABLED,
CHAINED_WARNING_DISABLED_INPLACE_METHOD,
IS64,
ISMUSL,
PY312,
PY314,
PYPY,
WARNING_CHECK_DISABLED,
WASM,
)
from pandas.compat.numpy import is_numpy_dev
Expand Down Expand Up @@ -152,14 +153,15 @@ def is_ci_environment() -> bool:


__all__ = [
"CHAINED_WARNING_DISABLED",
"CHAINED_WARNING_DISABLED_INPLACE_METHOD",
"HAS_PYARROW",
"IS64",
"ISMUSL",
"PY312",
"PY314",
"PYARROW_MIN_VERSION",
"PYPY",
"WARNING_CHECK_DISABLED",
"WASM",
"is_numpy_dev",
"pa_version_under14p0",
Expand Down
3 changes: 2 additions & 1 deletion pandas/compat/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
WASM = (sys.platform == "emscripten") or (platform.machine() in ["wasm32", "wasm64"])
ISMUSL = "musl" in (sysconfig.get_config_var("HOST_GNU_TYPE") or "")
REF_COUNT = 2
WARNING_CHECK_DISABLED = PY314
CHAINED_WARNING_DISABLED = PYPY or (PY314 and not sys._is_gil_enabled()) # type: ignore[attr-defined]
CHAINED_WARNING_DISABLED_INPLACE_METHOD = PYPY or PY314


__all__ = [
Expand Down
5 changes: 5 additions & 0 deletions pandas/compat/pickle_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
PeriodArray,
TimedeltaArray,
)
from pandas.core.generic import NDFrame
from pandas.core.internals import BlockManager

if TYPE_CHECKING:
Expand Down Expand Up @@ -90,6 +91,10 @@ def load_reduce(self) -> None:
cls = args[0]
stack[-1] = NDArrayBacked.__new__(*args)
return
elif args and issubclass(args[0], NDFrame):
cls = args[0]
stack[-1] = cls.__new__(cls)
return
raise

dispatch[pickle.REDUCE[0]] = load_reduce # type: ignore[assignment]
Expand Down
23 changes: 11 additions & 12 deletions pandas/core/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
properties,
)
from pandas._libs.hashtable import duplicated
from pandas._libs.internals import SetitemMixin
from pandas._libs.lib import is_range_indexer
from pandas.compat import PYPY
from pandas.compat._constants import (
CHAINED_WARNING_DISABLED_INPLACE_METHOD,
REF_COUNT,
WARNING_CHECK_DISABLED,
)
from pandas.compat._optional import import_optional_dependency
from pandas.compat.numpy import function as nv
Expand All @@ -63,7 +63,6 @@
)
from pandas.errors.cow import (
_chained_assignment_method_msg,
_chained_assignment_msg,
)
from pandas.util._decorators import (
Appender,
Expand Down Expand Up @@ -517,7 +516,7 @@


@set_module("pandas")
class DataFrame(NDFrame, OpsMixin):
class DataFrame(SetitemMixin, NDFrame, OpsMixin):
"""
Two-dimensional, size-mutable, potentially heterogeneous tabular data.

Expand Down Expand Up @@ -666,6 +665,11 @@ class DataFrame(NDFrame, OpsMixin):
# and ExtensionArray. Should NOT be overridden by subclasses.
__pandas_priority__ = 4000

# override those to avoid inheriting from SetitemMixin (cython generates
# them by default)
__reduce__ = object.__reduce__
__setstate__ = NDFrame.__setstate__

@property
def _constructor(self) -> type[DataFrame]:
return DataFrame
Expand Down Expand Up @@ -4267,7 +4271,8 @@ def isetitem(self, loc, value) -> None:
arraylike, refs = self._sanitize_column(value)
self._iset_item_mgr(loc, arraylike, inplace=False, refs=refs)

def __setitem__(self, key, value) -> None:
# def __setitem__() is implemented in SetitemMixin and dispatches to this method
def _setitem(self, key, value) -> None:
"""
Set item(s) in DataFrame by key.

Expand Down Expand Up @@ -4351,12 +4356,6 @@ def __setitem__(self, key, value) -> None:
z 3 50
# Values for 'a' and 'b' are completely ignored!
"""
if not PYPY and not WARNING_CHECK_DISABLED:
if sys.getrefcount(self) <= REF_COUNT + 1:
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
)

key = com.apply_if_callable(key, self)

# see if we can slice the rows
Expand Down Expand Up @@ -9314,7 +9313,7 @@ def update(
1 2 500.0
2 3 6.0
"""
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down
23 changes: 11 additions & 12 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@
WriteExcelBuffer,
npt,
)
from pandas.compat import PYPY
from pandas.compat._constants import (
CHAINED_WARNING_DISABLED_INPLACE_METHOD,
REF_COUNT,
WARNING_CHECK_DISABLED,
)
from pandas.compat._optional import import_optional_dependency
from pandas.compat.numpy import function as nv
Expand Down Expand Up @@ -2071,7 +2070,6 @@ def __getstate__(self) -> dict[str, Any]:
**meta,
}

@final
def __setstate__(self, state) -> None:
if isinstance(state, BlockManager):
self._mgr = state
Expand Down Expand Up @@ -4267,8 +4265,9 @@ def _slice(self, slobj: slice, axis: AxisInt = 0) -> Self:
result = result.__finalize__(self)
return result

# def __delitem__() is implemented in SetitemMixin and dispatches to this method
@final
def __delitem__(self, key) -> None:
def _delitem(self, key) -> None:
"""
Delete item
"""
Expand Down Expand Up @@ -7090,7 +7089,7 @@ def fillna(
"""
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -7337,7 +7336,7 @@ def ffill(
"""
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -7477,7 +7476,7 @@ def bfill(
"""
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -7562,7 +7561,7 @@ def replace(

inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -7925,7 +7924,7 @@ def interpolate(
inplace = validate_bool_kwarg(inplace, "inplace")

if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -8509,7 +8508,7 @@ def clip(
inplace = validate_bool_kwarg(inplace, "inplace")

if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -10152,7 +10151,7 @@ def where(
"""
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down Expand Up @@ -10216,7 +10215,7 @@ def mask(
) -> Self | None:
inplace = validate_bool_kwarg(inplace, "inplace")
if inplace:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED_INPLACE_METHOD:
if sys.getrefcount(self) <= REF_COUNT:
warnings.warn(
_chained_assignment_method_msg,
Expand Down
9 changes: 4 additions & 5 deletions pandas/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@

from pandas._libs.indexing import NDFrameIndexerBase
from pandas._libs.lib import item_from_zerodim
from pandas.compat import PYPY
from pandas.compat._constants import (
CHAINED_WARNING_DISABLED,
REF_COUNT,
WARNING_CHECK_DISABLED,
)
from pandas.errors import (
AbstractMethodError,
Expand Down Expand Up @@ -920,7 +919,7 @@ def _ensure_listlike_indexer(self, key, axis=None, value=None) -> None:

@final
def __setitem__(self, key, value) -> None:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED:
if sys.getrefcount(self.obj) <= REF_COUNT:
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
Expand Down Expand Up @@ -2588,7 +2587,7 @@ def __getitem__(self, key):
return super().__getitem__(key)

def __setitem__(self, key, value) -> None:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED:
if sys.getrefcount(self.obj) <= REF_COUNT:
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
Expand Down Expand Up @@ -2619,7 +2618,7 @@ def _convert_key(self, key):
return key

def __setitem__(self, key, value) -> None:
if not PYPY and not WARNING_CHECK_DISABLED:
if not CHAINED_WARNING_DISABLED:
if sys.getrefcount(self.obj) <= REF_COUNT:
warnings.warn(
_chained_assignment_msg, ChainedAssignmentError, stacklevel=2
Expand Down
Loading
Loading