dataframe-textual 1.5.0__py3-none-any.whl → 2.2.2__py3-none-any.whl

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.
@@ -5,11 +5,13 @@ from typing import TYPE_CHECKING
5
5
  if TYPE_CHECKING:
6
6
  from .data_frame_table import DataFrameTable
7
7
 
8
+
8
9
  import polars as pl
9
10
  from textual.app import ComposeResult
10
11
  from textual.containers import Container, Horizontal
11
12
  from textual.screen import ModalScreen
12
- from textual.widgets import Button, Checkbox, Input, Label, Static
13
+ from textual.widgets import Button, Checkbox, Input, Label, Static, TabPane
14
+ from textual.widgets.tabbed_content import ContentTab
13
15
 
14
16
  from .common import NULL, DtypeConfig, tentative_expr, validate_expr
15
17
 
@@ -117,9 +119,6 @@ class YesNoScreen(ModalScreen):
117
119
  maybe: Optional Maybe button text/dict. Defaults to None.
118
120
  no: Text or dict for the No button. If None, hides the No button. Defaults to "No".
119
121
  on_yes_callback: Optional callable that takes no args and returns the value to dismiss with when Yes is pressed. Defaults to None.
120
-
121
- Returns:
122
- None
123
122
  """
124
123
  super().__init__()
125
124
  self.title = title
@@ -254,7 +253,18 @@ class YesNoScreen(ModalScreen):
254
253
  def on_key(self, event) -> None:
255
254
  """Handle key press events in the table screen."""
256
255
  if event.key == "enter":
257
- self._handle_yes()
256
+ for button in self.query(Button):
257
+ if button.has_focus:
258
+ if button.id == "yes":
259
+ self._handle_yes()
260
+ elif button.id == "maybe":
261
+ self._handle_maybe()
262
+ elif button.id == "no":
263
+ self.dismiss(None)
264
+ break
265
+ else:
266
+ self._handle_yes()
267
+
258
268
  event.stop()
259
269
  elif event.key == "escape":
260
270
  self.dismiss(None)
@@ -282,10 +292,14 @@ class SaveFileScreen(YesNoScreen):
282
292
 
283
293
  CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SaveFileScreen")
284
294
 
285
- def __init__(self, filename: str, title="Save Tab"):
295
+ def __init__(self, filename: str, save_all: bool = False, tab_count: int = 1):
296
+ self.save_all = save_all
286
297
  super().__init__(
287
- title=title,
298
+ title="Save to File",
299
+ label="Filename",
288
300
  input=filename,
301
+ yes=f"Save {tab_count} Tabs" if self.save_all else "Save Current Tab" if tab_count > 1 else "Save",
302
+ no="Cancel",
289
303
  on_yes_callback=self.handle_save,
290
304
  )
291
305
 
@@ -293,13 +307,11 @@ class SaveFileScreen(YesNoScreen):
293
307
  if self.input:
294
308
  input_filename = self.input.value.strip()
295
309
  if input_filename:
296
- return input_filename
310
+ return input_filename, self.save_all, True # Overwrite prompt
297
311
  else:
298
312
  self.notify("Filename cannot be empty", title="Save", severity="error")
299
313
  return None
300
314
 
301
- return None
302
-
303
315
 
304
316
  class ConfirmScreen(YesNoScreen):
305
317
  """Modal screen to ask for confirmation."""
@@ -338,7 +350,7 @@ class EditCellScreen(YesNoScreen):
338
350
 
339
351
  # Input
340
352
  df_value = df.item(ridx, cidx)
341
- self.input_value = "" if df_value is None else str(df_value).strip()
353
+ self.input_value = NULL if df_value is None else str(df_value)
342
354
 
343
355
  super().__init__(
344
356
  title="Edit Cell",
@@ -352,20 +364,20 @@ class EditCellScreen(YesNoScreen):
352
364
 
353
365
  def _validate_input(self) -> None:
354
366
  """Validate and save the edited value."""
355
- new_value_str = self.input.value.strip()
367
+ new_value_str = self.input.value # Do not strip to preserve spaces
356
368
 
357
369
  # Handle empty input
358
370
  if not new_value_str:
359
- new_value = None
371
+ new_value = ""
360
372
  self.notify(
361
- "Empty value provided. If you want to clear the cell, press [$accent]x[/].",
362
- title="Edit",
373
+ "Empty value provided. If you want to clear the cell, press [$accent]Delete[/].",
374
+ title="Edit Cell",
363
375
  severity="warning",
364
376
  )
365
377
  # Check if value changed
366
378
  elif new_value_str == self.input_value:
367
379
  new_value = None
368
- self.notify("No changes made", title="Edit", severity="warning")
380
+ self.notify("No changes made", title="Edit Cell", severity="warning")
369
381
  else:
370
382
  # Parse and validate based on column dtype
371
383
  try:
@@ -373,7 +385,7 @@ class EditCellScreen(YesNoScreen):
373
385
  except Exception as e:
374
386
  self.notify(
375
387
  f"Failed to convert [$accent]{new_value_str}[/] to [$error]{self.dtype}[/]: {str(e)}",
376
- title="Edit",
388
+ title="Edit Cell",
377
389
  severity="error",
378
390
  )
379
391
  return None
@@ -456,7 +468,7 @@ class SearchScreen(YesNoScreen):
456
468
 
457
469
  def _validate_input(self) -> tuple[str, int, bool, bool]:
458
470
  """Validate the input and return it."""
459
- term = self.input.value.strip()
471
+ term = self.input.value # Do not strip to preserve spaces
460
472
 
461
473
  if not term:
462
474
  self.notify("Term cannot be empty", title=self.title, severity="error")
@@ -473,13 +485,13 @@ class FilterScreen(YesNoScreen):
473
485
 
474
486
  CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
475
487
 
476
- def __init__(self, df: pl.DataFrame, cidx: int, input_value: str | None = None):
488
+ def __init__(self, df: pl.DataFrame, cidx: int, term: str | None = None):
477
489
  self.df = df
478
490
  self.cidx = cidx
479
491
  super().__init__(
480
492
  title="Filter by Expression",
481
493
  label="e.g., NULL, $1 > 50, $name == 'text', $_ > 100, $a < $b, $_.str.contains('sub')",
482
- input=input_value,
494
+ input=term,
483
495
  checkbox="Match Nocase",
484
496
  checkbox2="Match Whole",
485
497
  on_yes_callback=self._get_input,
@@ -487,7 +499,7 @@ class FilterScreen(YesNoScreen):
487
499
 
488
500
  def _get_input(self) -> tuple[str, int, bool, bool]:
489
501
  """Get input."""
490
- term = self.input.value.strip()
502
+ term = self.input.value # Do not strip to preserve spaces
491
503
  match_nocase = self.checkbox.value
492
504
  match_whole = self.checkbox2.value
493
505
 
@@ -562,14 +574,14 @@ class EditColumnScreen(YesNoScreen):
562
574
  self.df = df
563
575
  super().__init__(
564
576
  title="Edit Column",
565
- label=f"by value or Polars expression, e.g., abc, pl.lit(7), {NULL}, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.arange(0, pl.len())",
577
+ label=f"By value or Polars expression, e.g., abc, pl.lit(7), {NULL}, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.arange(0, pl.len())",
566
578
  input="$_",
567
579
  on_yes_callback=self._get_input,
568
580
  )
569
581
 
570
582
  def _get_input(self) -> tuple[str, int]:
571
583
  """Get input."""
572
- term = self.input.value.strip()
584
+ term = self.input.value # Do not strip to preserve spaces
573
585
  return term, self.cidx
574
586
 
575
587
 
@@ -585,19 +597,19 @@ class AddColumnScreen(YesNoScreen):
585
597
  self.existing_columns = set(df.columns)
586
598
  super().__init__(
587
599
  title="Add Column",
588
- label="Enter column name",
589
- input="Link" if link else "Column name",
590
- label2="Enter link template, e.g., https://example.com/$1/id/$id, PC/compound/$_"
600
+ label="Column name",
601
+ input="Link" if link else "New column",
602
+ label2="Link template, e.g., https://example.com/$1/id/$_, PC/compound/$cid"
591
603
  if link
592
- else "Enter value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_.str.to_uppercase(), pl.concat_str($_, pl.lit('-suffix'))",
593
- input2="Link template" if link else "Column value or expression",
604
+ else "Value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_ + '_suffix', $_.str.to_uppercase()",
605
+ input2="Link template" if link else "Value or expression",
594
606
  on_yes_callback=self._get_input,
595
607
  )
596
608
 
597
609
  def _get_input(self) -> tuple[int, str, str] | None:
598
610
  """Validate and return the new column configuration."""
599
611
  col_name = self.input.value.strip()
600
- term = self.input2.value.strip()
612
+ term = self.input2.value # Do not strip to preserve spaces
601
613
 
602
614
  # Validate column name
603
615
  if not col_name:
@@ -659,7 +671,11 @@ class FindReplaceScreen(YesNoScreen):
659
671
  CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ReplaceScreen")
660
672
 
661
673
  def __init__(self, dftable: "DataFrameTable", title: str = "Find and Replace"):
662
- term_find = str(dftable.cursor_value)
674
+ if (cursor_value := dftable.cursor_value) is None:
675
+ term_find = NULL
676
+ else:
677
+ term_find = str(cursor_value)
678
+
663
679
  super().__init__(
664
680
  title=title,
665
681
  label="Find",
@@ -677,8 +693,8 @@ class FindReplaceScreen(YesNoScreen):
677
693
 
678
694
  def _get_input(self) -> tuple[str, str, bool, bool, bool]:
679
695
  """Get input."""
680
- term_find = self.input.value.strip()
681
- term_replace = self.input2.value.strip()
696
+ term_find = self.input.value # Do not strip to preserve spaces
697
+ term_replace = self.input2.value # Do not strip to preserve spaces
682
698
  match_nocase = self.checkbox.value
683
699
  match_whole = self.checkbox2.value
684
700
  replace_all = False
@@ -687,10 +703,50 @@ class FindReplaceScreen(YesNoScreen):
687
703
 
688
704
  def _get_input_replace_all(self) -> tuple[str, str, bool, bool, bool]:
689
705
  """Get input for 'Replace All'."""
690
- term_find = self.input.value.strip()
691
- term_replace = self.input2.value.strip()
706
+ term_find = self.input.value # Do not strip to preserve spaces
707
+ term_replace = self.input2.value # Do not strip to preserve spaces
692
708
  match_nocase = self.checkbox.value
693
709
  match_whole = self.checkbox2.value
694
710
  replace_all = True
695
711
 
696
712
  return term_find, term_replace, match_nocase, match_whole, replace_all
713
+
714
+
715
+ class RenameTabScreen(YesNoScreen):
716
+ """Modal screen to rename a tab."""
717
+
718
+ CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "RenameTabScreen")
719
+
720
+ def __init__(self, content_tab: ContentTab, existing_tabs: list[TabPane]):
721
+ self.content_tab = content_tab
722
+ self.existing_tabs = existing_tabs
723
+ tab_name = content_tab.label_text
724
+
725
+ super().__init__(
726
+ title="Rename Tab",
727
+ label="New tab name",
728
+ input={"value": tab_name},
729
+ on_yes_callback=self._validate_input,
730
+ )
731
+
732
+ def _validate_input(self) -> None:
733
+ """Validate and save the new tab name."""
734
+ new_name = self.input.value.strip()
735
+
736
+ # Check if name is empty
737
+ if not new_name:
738
+ self.notify("Tab name cannot be empty", title="Rename Tab", severity="error")
739
+ return None
740
+
741
+ # Check if name changed
742
+ if new_name == self.content_tab.label_text:
743
+ self.notify("No changes made", title="Rename Tab", severity="warning")
744
+ return None
745
+
746
+ # Check if name already exists
747
+ if new_name in self.existing_tabs:
748
+ self.notify(f"Tab [$accent]{new_name}[/] already exists", title="Rename Tab", severity="error")
749
+ return None
750
+
751
+ # Return new name
752
+ return self.content_tab, new_name