Repository: pandas-dev/pandas
Pull request #57668 (2024-02-28 23:30:02)

CLN: More numpy 2 stuff
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
- [ ] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [ ] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [ ] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.
- [ ] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.


WillAyd: I merged main thinking the CI failures were unrelated but looks like they are still around. Happy to merge as is given I am fairly confident the failures are unrelated, but will leave it up to you

Patch:
diff --git a/doc/source/whatsnew/v3.0.0.rst b/doc/source/whatsnew/v3.0.0.rst
index 9338d084f5..0f125af599 100644
--- a/doc/source/whatsnew/v3.0.0.rst
+++ b/doc/source/whatsnew/v3.0.0.rst
@@ -266,6 +266,7 @@ Bug fixes
 ~~~~~~~~~
 - Fixed bug in :meth:`DataFrame.join` inconsistently setting result index name (:issue:`55815`)
 - Fixed bug in :meth:`DataFrame.to_string` that raised ``StopIteration`` with nested DataFrames. (:issue:`16098`)
+- Fixed bug in :meth:`DataFrame.update` bool dtype being converted to object (:issue:`55509`)
 - Fixed bug in :meth:`Series.diff` allowing non-integer values for the ``periods`` argument. (:issue:`56607`)
 
 Categorical
diff --git a/pandas/_libs/src/vendored/ujson/python/objToJSON.c b/pandas/_libs/src/vendored/ujson/python/objToJSON.c
index 74ca8ead3d..fa91db5fe3 100644
--- a/pandas/_libs/src/vendored/ujson/python/objToJSON.c
+++ b/pandas/_libs/src/vendored/ujson/python/objToJSON.c
@@ -74,7 +74,6 @@ typedef struct __NpyArrContext {
   npy_intp ndim;
   npy_intp index[NPY_MAXDIMS];
   int type_num;
-  PyArray_GetItemFunc *getitem;
 
   char **rowLabels;
   char **columnLabels;
@@ -405,7 +404,6 @@ static void NpyArr_iterBegin(JSOBJ _obj, JSONTypeContext *tc) {
   }
 
   npyarr->array = (PyObject *)obj;
-  npyarr->getitem = (PyArray_GetItemFunc *)PyArray_DESCR(obj)->f->getitem;
   npyarr->dataptr = PyArray_DATA(obj);
   npyarr->ndim = PyArray_NDIM(obj) - 1;
   npyarr->curdim = 0;
@@ -492,7 +490,7 @@ static int NpyArr_iterNextItem(JSOBJ obj, JSONTypeContext *tc) {
     ((PyObjectEncoder *)tc->encoder)->npyValue = npyarr->dataptr;
     ((PyObjectEncoder *)tc->encoder)->npyCtxtPassthru = npyarr;
   } else {
-    GET_TC(tc)->itemValue = npyarr->getitem(npyarr->dataptr, npyarr->array);
+    GET_TC(tc)->itemValue = PyArray_GETITEM(arrayobj, npyarr->dataptr);
   }
 
   npyarr->dataptr += npyarr->stride;
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index f530466c0f..54cefabb60 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -8706,6 +8706,10 @@ class DataFrame(NDFrame, OpsMixin):
         dict.update : Similar method for dictionaries.
         DataFrame.merge : For column(s)-on-column(s) operations.
 
+        Notes
+        -----
+        1. Duplicate indices on `other` are not supported and raises `ValueError`.
+
         Examples
         --------
         >>> df = pd.DataFrame({"A": [1, 2, 3], "B": [400, 500, 600]})
@@ -8778,11 +8782,22 @@ class DataFrame(NDFrame, OpsMixin):
         if not isinstance(other, DataFrame):
             other = DataFrame(other)
 
-        other = other.reindex(self.index)
+        if other.index.has_duplicates:
+            raise ValueError("Update not allowed with duplicate indexes on other.")
+
+        index_intersection = other.index.intersection(self.index)
+        if index_intersection.empty:
+            raise ValueError(
+                "Update not allowed when the index on `other` has no intersection "
+                "with this dataframe."
+            )
+
+        other = other.reindex(index_intersection)
+        this_data = self.loc[index_intersection]
 
         for col in self.columns.intersection(other.columns):
-            this = self[col]._values
-            that = other[col]._values
+            this = this_data[col]
+            that = other[col]
 
             if filter_func is not None:
                 mask = ~filter_func(this) | isna(that)
@@ -8802,7 +8817,7 @@ class DataFrame(NDFrame, OpsMixin):
             if mask.all():
                 continue
 
-            self.loc[:, col] = self[col].where(mask, that)
+            self.loc[index_intersection, col] = this.where(mask, that)
 
     # ----------------------------------------------------------------------
     # Data reshaping


Test patch:
diff --git a/pandas/tests/frame/methods/test_update.py b/pandas/tests/frame/methods/test_update.py
index 788c6220b2..269b9e372b 100644
--- a/pandas/tests/frame/methods/test_update.py
+++ b/pandas/tests/frame/methods/test_update.py
@@ -184,3 +184,55 @@ class TestDataFrameUpdate:
             {"A": [1.0, 3.0], "B": [pd.NaT, pd.to_datetime("2016-01-01")]}
         )
         tm.assert_frame_equal(df, expected)
+
+    @pytest.mark.parametrize(
+        "value_df, value_other, dtype",
+        [
+            (True, False, bool),
+            (1, 2, int),
+            (1.0, 2.0, float),
+            (1.0 + 1j, 2.0 + 2j, complex),
+            (np.uint64(1), np.uint(2), np.dtype("ubyte")),
+            (np.uint64(1), np.uint(2), np.dtype("intc")),
+            ("a", "b", pd.StringDtype()),
+            (
+                pd.to_timedelta("1 ms"),
+                pd.to_timedelta("2 ms"),
+                np.dtype("timedelta64[ns]"),
+            ),
+            (
+                np.datetime64("2000-01-01T00:00:00"),
+                np.datetime64("2000-01-02T00:00:00"),
+                np.dtype("datetime64[ns]"),
+            ),
+        ],
+    )
+    def test_update_preserve_dtype(self, value_df, value_other, dtype):
+        # GH#55509
+        df = DataFrame({"a": [value_df] * 2}, index=[1, 2], dtype=dtype)
+        other = DataFrame({"a": [value_other]}, index=[1], dtype=dtype)
+        expected = DataFrame({"a": [value_other, value_df]}, index=[1, 2], dtype=dtype)
+        df.update(other)
+        tm.assert_frame_equal(df, expected)
+
+    def test_update_raises_on_duplicate_argument_index(self):
+        # GH#55509
+        df = DataFrame({"a": [1, 1]}, index=[1, 2])
+        other = DataFrame({"a": [2, 3]}, index=[1, 1])
+        with pytest.raises(ValueError, match="duplicate index"):
+            df.update(other)
+
+    def test_update_raises_without_intersection(self):
+        # GH#55509
+        df = DataFrame({"a": [1]}, index=[1])
+        other = DataFrame({"a": [2]}, index=[2])
+        with pytest.raises(ValueError, match="no intersection"):
+            df.update(other)
+
+    def test_update_on_duplicate_frame_unique_argument_index(self):
+        # GH#55509
+        df = DataFrame({"a": [1, 1, 1]}, index=[1, 1, 2], dtype=np.dtype("intc"))
+        other = DataFrame({"a": [2, 3]}, index=[1, 2], dtype=np.dtype("intc"))
+        expected = DataFrame({"a": [2, 2, 3]}, index=[1, 1, 2], dtype=np.dtype("intc"))
+        df.update(other)
+        tm.assert_frame_equal(df, expected)