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.
- dataframe_textual/__init__.py +27 -1
- dataframe_textual/__main__.py +14 -3
- dataframe_textual/common.py +154 -59
- dataframe_textual/data_frame_help_panel.py +0 -3
- dataframe_textual/data_frame_table.py +1910 -1238
- dataframe_textual/data_frame_viewer.py +354 -100
- dataframe_textual/sql_screen.py +56 -20
- dataframe_textual/table_screen.py +164 -144
- dataframe_textual/yes_no_screen.py +90 -34
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-2.2.2.dist-info}/METADATA +275 -416
- dataframe_textual-2.2.2.dist-info/RECORD +14 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-2.2.2.dist-info}/WHEEL +1 -1
- dataframe_textual-1.5.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-2.2.2.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-2.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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.
|
|
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,
|
|
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=
|
|
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 =
|
|
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
|
|
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 =
|
|
371
|
+
new_value = ""
|
|
360
372
|
self.notify(
|
|
361
|
-
"Empty value provided. If you want to clear the cell, press [$accent]
|
|
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
|
|
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,
|
|
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=
|
|
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
|
|
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"
|
|
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
|
|
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="
|
|
589
|
-
input="Link" if link else "
|
|
590
|
-
label2="
|
|
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 "
|
|
593
|
-
input2="Link template" if link else "
|
|
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
|
|
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
|
-
|
|
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
|
|
681
|
-
term_replace = self.input2.value
|
|
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
|
|
691
|
-
term_replace = self.input2.value
|
|
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
|