dataframe-textual 2.5.0__tar.gz → 2.6.2__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.2}/PKG-INFO +3 -2
  2. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/README.md +2 -1
  3. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/pyproject.toml +1 -1
  4. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/data_frame_table.py +104 -60
  5. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/uv.lock +1 -1
  6. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/.gitignore +0 -0
  7. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/1811.csv.gz +0 -0
  8. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/LICENSE +0 -0
  9. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/large_malformed.tsv.gz +0 -0
  10. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/main.py +0 -0
  11. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/__init__.py +0 -0
  12. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/__main__.py +0 -0
  13. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/common.py +0 -0
  14. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/data_frame_help_panel.py +0 -0
  15. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/data_frame_viewer.py +0 -0
  16. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/sql_screen.py +0 -0
  17. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/src/dataframe_textual/table_screen.py +0 -0
  18. {dataframe_textual-2.5.0 → dataframe_textual-2.6.2}/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.2
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.2"
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()
@@ -1749,13 +1755,14 @@ class DataFrameTable(DataTable):
1749
1755
  max_width = label_width
1750
1756
 
1751
1757
  # Scan through all loaded rows that are visible to find max width
1752
- for row_idx in range(self.loaded_rows):
1753
- cell_value = str(self.df.item(row_idx, col_idx))
1754
- cell_width = measure(self.app.console, cell_value, 1)
1758
+ for row_start, row_end in self.loaded_ranges:
1759
+ for row_idx in range(row_start, row_end):
1760
+ cell_value = str(self.df.item(row_idx, col_idx))
1761
+ cell_width = measure(self.app.console, cell_value, 1)
1755
1762
 
1756
- if cell_width > max_width:
1757
- need_expand = True
1758
- max_width = max(max_width, cell_width)
1763
+ if cell_width > max_width:
1764
+ need_expand = True
1765
+ max_width = cell_width
1759
1766
 
1760
1767
  if not need_expand:
1761
1768
  return
@@ -2043,25 +2050,10 @@ class DataFrameTable(DataTable):
2043
2050
  # Also update the view if applicable
2044
2051
  # Update the value of col_name in df_view using the value of col_name from df based on RID mapping between them
2045
2052
  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
- )
2053
+ # Get updated column from df
2054
+ lf_updated = self.df.lazy().select(RID, pl.col(col_name))
2055
+ # Update df_view by joining on RID
2056
+ self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
2065
2057
  except Exception as e:
2066
2058
  self.notify(
2067
2059
  f"Error applying expression: [$error]{term}[/] to column [$accent]{col_name}[/]",
@@ -2147,18 +2139,26 @@ class DataFrameTable(DataTable):
2147
2139
 
2148
2140
  # Update the cell to None in the dataframe
2149
2141
  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)
2142
+ self.df = (
2143
+ self.df.lazy()
2144
+ .with_columns(
2145
+ pl.when(pl.arange(0, len(self.df)) == ridx)
2146
+ .then(pl.lit(None))
2147
+ .otherwise(pl.col(col_name))
2148
+ .alias(col_name)
2149
+ )
2150
+ .collect()
2155
2151
  )
2156
2152
 
2157
2153
  # Also update the view if applicable
2158
2154
  if self.df_view is not None:
2159
2155
  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)
2156
+ self.df_view = (
2157
+ self.df_view.lazy()
2158
+ .with_columns(
2159
+ pl.when(pl.col(RID) == ridx_view).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2160
+ )
2161
+ .collect()
2162
2162
  )
2163
2163
 
2164
2164
  # Update the display
@@ -2177,7 +2177,43 @@ class DataFrameTable(DataTable):
2177
2177
  timeout=10,
2178
2178
  )
2179
2179
  self.log(f"Error clearing cell ({ridx}, {col_name}): {str(e)}")
2180
- raise e
2180
+
2181
+ def do_clear_column(self) -> None:
2182
+ """Clear the current column by setting all its values to None."""
2183
+ col_idx = self.cursor_column
2184
+ col_name = self.cursor_col_name
2185
+ value = self.cursor_value
2186
+
2187
+ # Add to history
2188
+ self.add_history(f"Cleared column [$success]{col_name}[/]", dirty=True)
2189
+
2190
+ try:
2191
+ # Update the entire column to None in the dataframe
2192
+ self.df = (
2193
+ self.df.lazy()
2194
+ .with_columns(
2195
+ pl.when(pl.col(col_name) == value).then(pl.lit(None)).otherwise(pl.col(col_name)).alias(col_name)
2196
+ )
2197
+ .collect()
2198
+ )
2199
+
2200
+ # Also update the view if applicable
2201
+ if self.df_view is not None:
2202
+ lf_updated = self.df.lazy().select(RID, pl.col(col_name))
2203
+ self.df_view = self.df_view.lazy().update(lf_updated, on=RID, include_nulls=True).collect()
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."""
@@ -2961,10 +2997,10 @@ class DataFrameTable(DataTable):
2961
2997
  # self.notify("No selections to clear", title="Clear Selections and Matches", severity="warning")
2962
2998
  return
2963
2999
 
2964
- row_count = len(self.selected_rows | set(self.matches.keys()))
3000
+ # row_count = len(self.selected_rows | set(self.matches.keys()))
2965
3001
 
2966
3002
  # Add to history
2967
- self.add_history("Cleared all selected rows")
3003
+ self.add_history("Cleared all selections and matches")
2968
3004
 
2969
3005
  # Clear all selections
2970
3006
  self.selected_rows = set()
@@ -2973,7 +3009,7 @@ class DataFrameTable(DataTable):
2973
3009
  # Recreate table for display
2974
3010
  self.setup_table()
2975
3011
 
2976
- self.notify(f"Cleared selections for [$success]{row_count}[/] rows", title="Clear Selections and Matches")
3012
+ # self.notify(f"Cleared selections for [$success]{row_count}[/] rows", title="Clear Selections and Matches")
2977
3013
 
2978
3014
  # Find & Replace
2979
3015
  def find_matches(
@@ -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.2"
175
175
  source = { editable = "." }
176
176
  dependencies = [
177
177
  { name = "polars" },