|
8 | 8 | pd.set_option("future.no_silent_downcasting", True) |
9 | 9 |
|
10 | 10 |
|
| 11 | +def _convert_dtype_with_nullable_int(series, dtype): |
| 12 | + """Convert a series to the specified dtype, handling nullable integers. |
| 13 | +
|
| 14 | + When converting to integer types and the series contains NaN values, |
| 15 | + this function automatically uses pandas nullable integer types (Int64, Int32, etc.) |
| 16 | + instead of numpy integer types which don't support NaN. |
| 17 | +
|
| 18 | + Args: |
| 19 | + series: The pandas Series to convert |
| 20 | + dtype: The target dtype |
| 21 | +
|
| 22 | + Returns: |
| 23 | + The series with the appropriate dtype applied |
| 24 | + """ |
| 25 | + # Check if it's already a pandas nullable int type |
| 26 | + is_pandas_nullable_int = isinstance( |
| 27 | + dtype, |
| 28 | + (pd.Int64Dtype, pd.Int32Dtype, pd.Int16Dtype, pd.Int8Dtype), |
| 29 | + ) |
| 30 | + |
| 31 | + if is_pandas_nullable_int: |
| 32 | + # Keep pandas nullable integer types as is |
| 33 | + return series.astype(dtype) |
| 34 | + # If column has NaN and dtype is integer, use nullable Int type |
| 35 | + if np.issubdtype(dtype, np.integer) and series.isna().any(): |
| 36 | + # Convert numpy int types to pandas nullable Int types |
| 37 | + if dtype == np.int64: |
| 38 | + return series.astype("Int64") |
| 39 | + if dtype == np.int32: |
| 40 | + return series.astype("Int32") |
| 41 | + if dtype == np.int16: |
| 42 | + return series.astype("Int16") |
| 43 | + if dtype == np.int8: |
| 44 | + return series.astype("Int8") |
| 45 | + # Fallback for other integer types |
| 46 | + return series.astype("Int64") |
| 47 | + return series.astype(dtype) |
| 48 | + |
| 49 | + |
11 | 50 | class ModifyColumnCommand(QUndoCommand): |
12 | 51 | """Command to add or remove a column in the table. |
13 | 52 |
|
@@ -155,7 +194,9 @@ def redo(self): |
155 | 194 | if np.any(dtypes != df.dtypes): |
156 | 195 | for col, dtype in dtypes.items(): |
157 | 196 | if dtype != df.dtypes[col]: |
158 | | - df[col] = df[col].astype(dtype) |
| 197 | + df[col] = _convert_dtype_with_nullable_int( |
| 198 | + df[col], dtype |
| 199 | + ) |
159 | 200 | self.model.endInsertRows() |
160 | 201 | else: |
161 | 202 | self.model.beginRemoveRows( |
@@ -261,10 +302,17 @@ def _apply_changes(self, use_new: bool): |
261 | 302 | for col, dtype in original_dtypes.items(): |
262 | 303 | if col not in update_df.columns: |
263 | 304 | continue |
264 | | - if np.issubdtype(dtype, np.number): |
| 305 | + |
| 306 | + # For numeric types, convert string inputs to numbers first |
| 307 | + is_pandas_nullable_int = isinstance( |
| 308 | + dtype, |
| 309 | + (pd.Int64Dtype, pd.Int32Dtype, pd.Int16Dtype, pd.Int8Dtype), |
| 310 | + ) |
| 311 | + if is_pandas_nullable_int or np.issubdtype(dtype, np.number): |
265 | 312 | df[col] = pd.to_numeric(df[col], errors="coerce") |
266 | | - else: |
267 | | - df[col] = df[col].astype(dtype) |
| 313 | + |
| 314 | + # Convert to appropriate dtype, handling nullable integers |
| 315 | + df[col] = _convert_dtype_with_nullable_int(df[col], dtype) |
268 | 316 |
|
269 | 317 | rows = [df.index.get_loc(row_key) for (row_key, _) in self.changes] |
270 | 318 | cols = [ |
|
0 commit comments