dataframe-textual 2.5.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/PKG-INFO +3 -2
  2. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/README.md +2 -1
  3. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/pyproject.toml +1 -1
  4. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_table.py +95 -51
  5. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/uv.lock +1 -1
  6. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/.gitignore +0 -0
  7. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/1811.csv.gz +0 -0
  8. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/LICENSE +0 -0
  9. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/large_malformed.tsv.gz +0 -0
  10. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/main.py +0 -0
  11. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/__init__.py +0 -0
  12. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/__main__.py +0 -0
  13. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/common.py +0 -0
  14. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_help_panel.py +0 -0
  15. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/data_frame_viewer.py +0 -0
  16. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/sql_screen.py +0 -0
  17. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/table_screen.py +0 -0
  18. {dataframe_textual-2.5.0 → dataframe_textual-2.6.0}/src/dataframe_textual/yes_no_screen.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: Interactive terminal viewer/editor for tabular data
5
5
  Project-URL: Homepage, https://github.com/need47/dataframe-textual
6
6
  Project-URL: Repository, https://github.com/need47/dataframe-textual.git
@@ -314,7 +314,8 @@ zcat compressed_data.csv.gz | dv -f csv
314
314
  | Key | Action |
315
315
  |-----|--------|
316
316
  | `Double-click` | Edit cell or rename column header |
317
- | `delete` | Clear current cell (set to NULL) |
317
+ | `Delete` | Clear current cell (set to NULL) |
318
+ | `Shift+Delete` | Clear current column (set matching cells to NULL) |
318
319
  | `e` | Edit current cell (respects data type) |
319
320
  | `E` | Edit entire column with value/expression |
320
321
  | `a` | Add empty column after current |
@@ -275,7 +275,8 @@ zcat compressed_data.csv.gz | dv -f csv
275
275
  | Key | Action |
276
276
  |-----|--------|
277
277
  | `Double-click` | Edit cell or rename column header |
278
- | `delete` | Clear current cell (set to NULL) |
278
+ | `Delete` | Clear current cell (set to NULL) |
279
+ | `Shift+Delete` | Clear current column (set matching cells to NULL) |
279
280
  | `e` | Edit current cell (respects data type) |
280
281
  | `E` | Edit entire column with value/expression |
281
282
  | `a` | Add empty column after current |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dataframe-textual"
7
- version = "2.5.0"
7
+ version = "2.6.0"
8
8
  description = "Interactive terminal viewer/editor for tabular data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -207,6 +207,7 @@ class DataFrameTable(DataTable):
207
207
  - **X** - ❌ Delete row and those below
208
208
  - **Ctrl+X** - ❌ Delete row and those above
209
209
  - **delete** - ❌ Clear current cell (set to NULL)
210
+ - **Shift+Delete** - ❌ Clear current column (set matching cells to NULL)
210
211
  - **-** - ❌ Delete current column
211
212
  - **d** - 📋 Duplicate current column
212
213
  - **D** - 📋 Duplicate current row
@@ -285,6 +286,7 @@ class DataFrameTable(DataTable):
285
286
  ("R", "replace_global", "Replace global"), # `Shift+R`
286
287
  # Delete
287
288
  ("delete", "clear_cell", "Clear cell"),
289
+ ("shift+delete", "clear_column", "Clear cells in current column that match cursor value"), # `Shift+Delete`
288
290
  ("minus", "delete_column", "Delete column"), # `-`
289
291
  ("x", "delete_row", "Delete row"),
290
292
  ("X", "delete_row_and_below", "Delete row and those below"),
@@ -795,6 +797,10 @@ class DataFrameTable(DataTable):
795
797
  """Clear the current cell (set to None)."""
796
798
  self.do_clear_cell()
797
799
 
800
+ def action_clear_column(self) -> None:
801
+ """Clear cells in the current column that match the cursor value."""
802
+ self.do_clear_column()
803
+
798
804
  def action_select_row(self) -> None:
799
805
  """Select rows with cursor value in the current column."""
800
806
  self.do_select_row()
@@ -2043,25 +2049,10 @@ class DataFrameTable(DataTable):
2043
2049
  # Also update the view if applicable
2044
2050
  # Update the value of col_name in df_view using the value of col_name from df based on RID mapping between them
2045
2051
  if self.df_view is not None:
2046
- # Get updated column from df for rows that exist in df_view
2047
- col_updated = f"^_{col_name}_^"
2048
- col_exists = "^_exists_^"
2049
- lf_updated = self.df.lazy().select(
2050
- RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
2051
- )
2052
- # Join and use when/then/otherwise to handle all updates including NULLs
2053
- self.df_view = (
2054
- self.df_view.lazy()
2055
- .join(lf_updated, on=RID, how="left")
2056
- .with_columns(
2057
- pl.when(pl.col(col_exists))
2058
- .then(pl.col(col_updated))
2059
- .otherwise(pl.col(col_name))
2060
- .alias(col_name)
2061
- )
2062
- .drop(col_updated, col_exists)
2063
- .collect()
2064
- )
2052
+ # Get updated column from df
2053
+ lf_updated = self.df.lazy().select(RID, pl.col(col_name))
2054
+ # Update df_view by joining on RID
2055
+ self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
2065
2056
  except Exception as e:
2066
2057
  self.notify(
2067
2058
  f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
@@ -2147,18 +2138,26 @@ class DataFrameTable(DataTable):
2147
2138
 
2148
2139
  # Update the cell to None in the dataframe
2149
2140
  try:
2150
- self.df = self.df.with_columns(
2151
- pl.when(pl.arange(0, len(self.df)) == ridx)
2152
- .then(pl.lit(None))
2153
- .otherwise(pl.col(col_name))
2154
- .alias(col_name)
2141
+ self.df = (
2142
+ self.df.lazy()
2143
+ .with_columns(
2144
+ pl.when(pl.arange(0, len(self.df)) == ridx)
2145
+ .then(pl.lit(None))
2146
+ .otherwise(pl.col(col_name))
2147
+ .alias(col_name)
2148
+ )
2149
+ .collect()
2155
2150
  )
2156
2151
 
2157
2152
  # Also update the view if applicable
2158
2153
  if self.df_view is not None:
2159
2154
  ridx_view = self.df.item(ridx, self.df.columns.index(RID))
2160
- self.df_view = self.df_view.with_columns(
2161
- pl.when(pl.col(RID) == ridx_view).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2155
+ self.df_view = (
2156
+ self.df_view.lazy()
2157
+ .with_columns(
2158
+ pl.when(pl.col(RID) == ridx_view).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2159
+ )
2160
+ .collect()
2162
2161
  )
2163
2162
 
2164
2163
  # Update the display
@@ -2177,7 +2176,44 @@ class DataFrameTable(DataTable):
2177
2176
  timeout=10,
2178
2177
  )
2179
2178
  self.log(f"Error clearing cell ({ridx}, {col_name}): {str(e)}")
2180
- raise e
2179
+
2180
+ def do_clear_column(self) -> None:
2181
+ """Clear the current column by setting all its values to None."""
2182
+ col_idx = self.cursor_column
2183
+ col_name = self.cursor_col_name
2184
+ value = self.cursor_value
2185
+
2186
+ # Add to history
2187
+ self.add_history(f"Cleared column [$success]{col_name}[/]", dirty=True)
2188
+
2189
+ try:
2190
+ # Update the entire column to None in the dataframe
2191
+ self.df = (
2192
+ self.df.lazy()
2193
+ .with_columns(
2194
+ pl.when(pl.col(col_name) == value).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2195
+ )
2196
+ .collect()
2197
+ )
2198
+
2199
+ # Also update the view if applicable
2200
+ if self.df_view is not None:
2201
+ self.df_view = self.df_view.with_columns(
2202
+ pl.when(pl.col(col_name) == value).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2203
+ )
2204
+
2205
+ # Recreate table for display
2206
+ self.setup_table()
2207
+
2208
+ # Move cursor to the cleared column
2209
+ self.move_cursor(column=col_idx)
2210
+
2211
+ # self.notify(f"Cleared column [$success]{col_name}[/]", title="Clear Column")
2212
+ except Exception as e:
2213
+ self.notify(
2214
+ f"Error clearing column [$error]{col_name}[/]", title="Clear Column", severity="error", timeout=10
2215
+ )
2216
+ self.log(f"Error clearing column `{col_name}`: {str(e)}")
2181
2217
 
2182
2218
  def do_add_column(self, col_name: str = None) -> None:
2183
2219
  """Add acolumn after the current column."""
@@ -3385,18 +3421,23 @@ class DataFrameTable(DataTable):
3385
3421
  # Only applicable to string columns for substring matches
3386
3422
  if dtype == pl.String and not state.match_whole:
3387
3423
  term_find = f"(?i){state.term_find}" if state.match_nocase else state.term_find
3424
+ new_value = (
3425
+ pl.lit(None)
3426
+ if state.term_replace == NULL
3427
+ else pl.col(col_name).str.replace_all(term_find, state.term_replace)
3428
+ )
3388
3429
  self.df = self.df.with_columns(
3389
- pl.when(mask)
3390
- .then(pl.col(col_name).str.replace_all(term_find, state.term_replace))
3391
- .otherwise(pl.col(col_name))
3392
- .alias(col_name)
3430
+ pl.when(mask).then(new_value).otherwise(pl.col(col_name)).alias(col_name)
3393
3431
  )
3394
3432
  else:
3395
- # Try to convert replacement value to column dtype
3396
- try:
3397
- value = DtypeConfig(dtype).convert(state.term_replace)
3398
- except Exception:
3399
- value = state.term_replace
3433
+ if state.term_replace == NULL:
3434
+ value = None
3435
+ else:
3436
+ # Try to convert replacement value to column dtype
3437
+ try:
3438
+ value = DtypeConfig(dtype).convert(state.term_replace)
3439
+ except Exception:
3440
+ value = state.term_replace
3400
3441
 
3401
3442
  self.df = self.df.with_columns(
3402
3443
  pl.when(mask).then(pl.lit(value)).otherwise(pl.col(col_name)).alias(col_name)
@@ -3404,15 +3445,8 @@ class DataFrameTable(DataTable):
3404
3445
 
3405
3446
  # Also update the view if applicable
3406
3447
  if self.df_view is not None:
3407
- col_updated = f"^_{col_name}_^"
3408
- lf_updated = self.df.lazy().filter(mask).select(pl.col(col_name).alias(col_updated), pl.col(RID))
3409
- self.df_view = (
3410
- self.df_view.lazy()
3411
- .join(lf_updated, on=RID, how="left")
3412
- .with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
3413
- .drop(col_updated)
3414
- .collect()
3415
- )
3448
+ lf_updated = self.df.lazy().filter(mask).select(pl.col(RID), pl.col(col_name))
3449
+ self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
3416
3450
 
3417
3451
  state.replaced_occurrence += len(ridxs)
3418
3452
 
@@ -3491,9 +3525,14 @@ class DataFrameTable(DataTable):
3491
3525
  # Only applicable to string columns for substring matches
3492
3526
  if dtype == pl.String and not state.match_whole:
3493
3527
  term_find = f"(?i){state.term_find}" if state.match_nocase else state.term_find
3528
+ new_value = (
3529
+ pl.lit(None)
3530
+ if state.term_replace == NULL
3531
+ else pl.col(col_name).str.replace_all(term_find, state.term_replace)
3532
+ )
3494
3533
  self.df = self.df.with_columns(
3495
3534
  pl.when(pl.arange(0, len(self.df)) == ridx)
3496
- .then(pl.col(col_name).str.replace_all(term_find, state.term_replace))
3535
+ .then(new_value)
3497
3536
  .otherwise(pl.col(col_name))
3498
3537
  .alias(col_name)
3499
3538
  )
@@ -3507,11 +3546,14 @@ class DataFrameTable(DataTable):
3507
3546
  .alias(col_name)
3508
3547
  )
3509
3548
  else:
3510
- # try to convert replacement value to column dtype
3511
- try:
3512
- value = DtypeConfig(dtype).convert(state.term_replace)
3513
- except Exception:
3514
- value = state.term_replace
3549
+ if state.term_replace == NULL:
3550
+ value = None
3551
+ else:
3552
+ # try to convert replacement value to column dtype
3553
+ try:
3554
+ value = DtypeConfig(dtype).convert(state.term_replace)
3555
+ except Exception:
3556
+ value = state.term_replace
3515
3557
 
3516
3558
  self.df = self.df.with_columns(
3517
3559
  pl.when(pl.arange(0, len(self.df)) == ridx)
@@ -3539,6 +3581,8 @@ class DataFrameTable(DataTable):
3539
3581
  if not state.done:
3540
3582
  # Get the new value of the current cell after replacement
3541
3583
  new_cell_value = self.df.item(ridx, cidx)
3584
+ if new_cell_value is None:
3585
+ new_cell_value = NULL_DISPLAY
3542
3586
  row_key = str(ridx)
3543
3587
  col_key = col_name
3544
3588
  self.update_cell(
@@ -171,7 +171,7 @@ wheels = [
171
171
 
172
172
  [[package]]
173
173
  name = "dataframe-textual"
174
- version = "2.5.0"
174
+ version = "2.6.0"
175
175
  source = { editable = "." }
176
176
  dependencies = [
177
177
  { name = "polars" },