dataframe-textual 1.12.0__py3-none-any.whl → 2.0.0__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.
@@ -1,15 +1,32 @@
1
1
  """DataFrame Viewer - Interactive CSV/Excel viewer for the terminal."""
2
2
 
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("dataframe-textual")
6
+
3
7
  from .data_frame_help_panel import DataFrameHelpPanel
4
8
  from .data_frame_table import DataFrameTable, History
5
9
  from .data_frame_viewer import DataFrameViewer
6
- from .table_screen import FrequencyScreen, RowDetailScreen, TableScreen
10
+ from .table_screen import (
11
+ FrequencyScreen,
12
+ MetaColumnScreen,
13
+ MetaShape,
14
+ RowDetailScreen,
15
+ StatisticsScreen,
16
+ TableScreen,
17
+ )
7
18
  from .yes_no_screen import (
19
+ AddColumnScreen,
20
+ AddLinkScreen,
8
21
  ConfirmScreen,
9
22
  EditCellScreen,
23
+ EditColumnScreen,
10
24
  FilterScreen,
25
+ FindReplaceScreen,
11
26
  FreezeScreen,
12
27
  OpenFileScreen,
28
+ RenameColumnScreen,
29
+ RenameTabScreen,
13
30
  SaveFileScreen,
14
31
  SearchScreen,
15
32
  YesNoScreen,
@@ -23,6 +40,9 @@ __all__ = [
23
40
  "TableScreen",
24
41
  "RowDetailScreen",
25
42
  "FrequencyScreen",
43
+ "StatisticsScreen",
44
+ "MetaShape",
45
+ "MetaColumnScreen",
26
46
  "YesNoScreen",
27
47
  "SaveFileScreen",
28
48
  "ConfirmScreen",
@@ -31,4 +51,10 @@ __all__ = [
31
51
  "FilterScreen",
32
52
  "FreezeScreen",
33
53
  "OpenFileScreen",
54
+ "RenameColumnScreen",
55
+ "EditColumnScreen",
56
+ "AddColumnScreen",
57
+ "AddLinkScreen",
58
+ "FindReplaceScreen",
59
+ "RenameTabScreen",
34
60
  ]
@@ -4,6 +4,7 @@ import argparse
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
+ from . import __version__
7
8
  from .common import SUPPORTED_FORMATS, load_dataframe
8
9
  from .data_frame_viewer import DataFrameViewer
9
10
 
@@ -24,6 +25,12 @@ def cli() -> argparse.Namespace:
24
25
  " cat data.csv | %(prog)s --format csv\n",
25
26
  )
26
27
  parser.add_argument("files", nargs="*", help="Files to view (or read from stdin)")
28
+ parser.add_argument(
29
+ "-V",
30
+ "--version",
31
+ action="version",
32
+ version=f"%(prog)s {__version__}",
33
+ )
27
34
  parser.add_argument(
28
35
  "-f",
29
36
  "--format",
@@ -37,7 +44,7 @@ def cli() -> argparse.Namespace:
37
44
  help="Specify that input files have no header row when reading CSV/TSV",
38
45
  )
39
46
  parser.add_argument(
40
- "-I", "--no-inferrence", action="store_true", help="Do not infer data types when reading CSV/TSV"
47
+ "-I", "--no-inference", action="store_true", help="Do not infer data types when reading CSV/TSV"
41
48
  )
42
49
  parser.add_argument("-E", "--ignore-errors", action="store_true", help="Ignore errors when reading CSV/TSV")
43
50
  parser.add_argument(
@@ -80,7 +87,7 @@ def main() -> None:
80
87
  args.files,
81
88
  file_format=args.format,
82
89
  has_header=not args.no_header,
83
- infer_schema=not args.no_inferrence,
90
+ infer_schema=not args.no_inference,
84
91
  comment_prefix=args.comment_prefix,
85
92
  quote_char=args.quote_char,
86
93
  skip_lines=args.skip_lines,
@@ -34,6 +34,29 @@ NULL = "NULL"
34
34
  NULL_DISPLAY = "-"
35
35
 
36
36
 
37
+ def format_float(value: float, thousand_separator: bool = False, precision: int = 2) -> str:
38
+ """Format a float value, keeping integers without decimal point.
39
+
40
+ Args:
41
+ val: The float value to format.
42
+ thousand_separator: Whether to include thousand separators. Defaults to False.
43
+
44
+ Returns:
45
+ The formatted float as a string.
46
+ """
47
+
48
+ if (val := int(value)) == value:
49
+ if precision > 0:
50
+ return f"{val:,}" if thousand_separator else str(val)
51
+ else:
52
+ return f"{val:,.{-precision}f}" if thousand_separator else f"{val:.{-precision}f}"
53
+ else:
54
+ if precision > 0:
55
+ return f"{value:,.{precision}f}" if thousand_separator else f"{value:.{precision}f}"
56
+ else:
57
+ return f"{value:,f}" if thousand_separator else str(value)
58
+
59
+
37
60
  @dataclass
38
61
  class DtypeClass:
39
62
  """Data type class configuration.
@@ -52,6 +75,35 @@ class DtypeClass:
52
75
  itype: str
53
76
  convert: Any
54
77
 
78
+ def format(
79
+ self, val: Any, style: str | None = None, justify: str | None = None, thousand_separator: bool = False
80
+ ) -> str:
81
+ """Format the value according to its data type.
82
+
83
+ Args:
84
+ val: The value to format.
85
+
86
+ Returns:
87
+ The formatted value as a Text.
88
+ """
89
+ # Format the value
90
+ if val is None:
91
+ text_val = NULL_DISPLAY
92
+ elif self.gtype == "integer" and thousand_separator:
93
+ text_val = f"{val:,}"
94
+ elif self.gtype == "float":
95
+ text_val = format_float(val, thousand_separator)
96
+ else:
97
+ text_val = str(val)
98
+
99
+ return Text(
100
+ text_val,
101
+ style="" if style == "" else (style or self.style),
102
+ justify="" if justify == "" else (justify or self.justify),
103
+ overflow="ellipsis",
104
+ no_wrap=True,
105
+ )
106
+
55
107
 
56
108
  # itype is used by Input widget for input validation
57
109
  # fmt: off
@@ -100,8 +152,8 @@ SUBSCRIPT_DIGITS = {
100
152
  # Cursor types ("none" removed)
101
153
  CURSOR_TYPES = ["row", "column", "cell"]
102
154
 
103
- # For row index column
104
- RIDX = "^_ridx_^"
155
+ # Row index mapping between filtered and original dataframe
156
+ RID = "^_RID_^"
105
157
 
106
158
 
107
159
  @dataclass
@@ -143,27 +195,7 @@ def DtypeConfig(dtype: pl.DataType) -> DtypeClass:
143
195
  return STYLES[pl.Unknown]
144
196
 
145
197
 
146
- def format_float(value: float, thousand_separator: bool = False, precision: int = 2) -> str:
147
- """Format a float value, keeping integers without decimal point.
148
-
149
- Args:
150
- val: The float value to format.
151
- thousand_separator: Whether to include thousand separators. Defaults to False.
152
-
153
- Returns:
154
- The formatted float as a string.
155
- """
156
-
157
- if (val := int(value)) == value:
158
- return f"{val:,}" if thousand_separator else str(val)
159
- else:
160
- if precision > 0:
161
- return f"{value:,.{precision}f}" if thousand_separator else f"{value:.{precision}f}"
162
- else:
163
- return f"{value:,f}" if thousand_separator else str(value)
164
-
165
-
166
- def format_row(vals, dtypes, styles=None, apply_justify=True, thousand_separator=False) -> list[Text]:
198
+ def format_row(vals, dtypes, styles: list[str | None] | None = None, thousand_separator=False) -> list[Text]:
167
199
  """Format a single row with proper styling and justification.
168
200
 
169
201
  Converts raw row values to formatted Rich Text objects with appropriate
@@ -172,7 +204,7 @@ def format_row(vals, dtypes, styles=None, apply_justify=True, thousand_separator
172
204
  Args:
173
205
  vals: The list of values in the row.
174
206
  dtypes: The list of data types corresponding to each value.
175
- apply_justify: Whether to apply justification styling. Defaults to True.
207
+ styles: Optional list of style overrides for each value. Defaults to None.
176
208
 
177
209
  Returns:
178
210
  A list of Rich Text objects with proper formatting applied.
@@ -181,31 +213,18 @@ def format_row(vals, dtypes, styles=None, apply_justify=True, thousand_separator
181
213
 
182
214
  for idx, (val, dtype) in enumerate(zip(vals, dtypes, strict=True)):
183
215
  dc = DtypeConfig(dtype)
184
-
185
- # Format the value
186
- if val is None:
187
- text_val = NULL_DISPLAY
188
- elif dc.gtype == "integer" and thousand_separator:
189
- text_val = f"{val:,}"
190
- elif dc.gtype == "float":
191
- text_val = format_float(val, thousand_separator)
192
- else:
193
- text_val = str(val)
194
-
195
216
  formatted_row.append(
196
- Text(
197
- text_val,
198
- style=styles[idx] if styles and styles[idx] else dc.style,
199
- justify=dc.justify if apply_justify else "",
200
- overflow="ellipsis",
201
- no_wrap=True,
217
+ dc.format(
218
+ val,
219
+ style=styles[idx] if styles and styles[idx] else None,
220
+ thousand_separator=thousand_separator,
202
221
  )
203
222
  )
204
223
 
205
224
  return formatted_row
206
225
 
207
226
 
208
- def rindex(lst: list, value) -> int:
227
+ def rindex(lst: list, value, pos: int | None = None) -> int:
209
228
  """Return the last index of value in a list. Return -1 if not found.
210
229
 
211
230
  Searches through the list in reverse order to find the last occurrence
@@ -218,9 +237,12 @@ def rindex(lst: list, value) -> int:
218
237
  Returns:
219
238
  The index (0-based) of the last occurrence, or -1 if not found.
220
239
  """
240
+ n = len(lst)
221
241
  for i, item in enumerate(reversed(lst)):
242
+ if pos is not None and (n - 1 - i) > pos:
243
+ continue
222
244
  if item == value:
223
- return len(lst) - 1 - i
245
+ return n - 1 - i
224
246
  return -1
225
247
 
226
248
 
@@ -253,9 +275,10 @@ def parse_placeholders(template: str, columns: list[str], current_cidx: int) ->
253
275
 
254
276
  Supports multiple placeholder types:
255
277
  - `$_` - Current column (based on current_cidx parameter)
256
- - `$#` - Row index (1-based, requires '^__ridx__^' column to be present)
278
+ - `$#` - Row index (1-based)
257
279
  - `$1`, `$2`, etc. - Column index (1-based)
258
280
  - `$name` - Column name (e.g., `$product_id`)
281
+ - `` $`col name` `` - Column name with spaces (e.g., `` $`product id` ``)
259
282
 
260
283
  Args:
261
284
  template: The template string containing placeholders and literal text
@@ -271,8 +294,15 @@ def parse_placeholders(template: str, columns: list[str], current_cidx: int) ->
271
294
  if "$" not in template or template.endswith("$"):
272
295
  return [template]
273
296
 
274
- # Regex matches: $_ or $\d+ or $\w+ (column names)
275
- placeholder_pattern = r"\$(_|#|\d+|[a-zA-Z_]\w*)"
297
+ # Regex matches: $_ or $# or $\d+ or $`...` (backtick-quoted names with spaces) or $\w+ (column names)
298
+ # Pattern explanation:
299
+ # \$(_|#|\d+|`[^`]+`|[a-zA-Z_]\w*)
300
+ # - $_ : current column
301
+ # - $# : row index
302
+ # - $\d+ : column by index (1-based)
303
+ # - $`[^`]+` : column by name with spaces (backtick quoted)
304
+ # - $[a-zA-Z_]\w* : column by name without spaces
305
+ placeholder_pattern = r"\$(_|#|\d+|`[^`]+`|[a-zA-Z_]\w*)"
276
306
  placeholders = re.finditer(placeholder_pattern, template)
277
307
 
278
308
  parts = []
@@ -296,7 +326,7 @@ def parse_placeholders(template: str, columns: list[str], current_cidx: int) ->
296
326
  parts.append(pl.col(col_name))
297
327
  elif placeholder == "#":
298
328
  # $# refers to row index (1-based)
299
- parts.append((pl.col(RIDX)))
329
+ parts.append(pl.col(RID))
300
330
  elif placeholder.isdigit():
301
331
  # $1, $2, etc. refer to columns by 1-based position index
302
332
  col_idx = int(placeholder) - 1 # Convert to 0-based
@@ -305,6 +335,13 @@ def parse_placeholders(template: str, columns: list[str], current_cidx: int) ->
305
335
  parts.append(pl.col(col_ref))
306
336
  except IndexError:
307
337
  raise ValueError(f"Invalid column index: ${placeholder} (valid range: $1 to ${len(columns)})")
338
+ elif placeholder.startswith("`") and placeholder.endswith("`"):
339
+ # $`col name` refers to column by name with spaces
340
+ col_ref = placeholder[1:-1] # Remove backticks
341
+ if col_ref in columns:
342
+ parts.append(pl.col(col_ref))
343
+ else:
344
+ raise ValueError(f"Column not found: ${placeholder} (available columns: {', '.join(columns)})")
308
345
  else:
309
346
  # $name refers to column by name
310
347
  if placeholder in columns:
@@ -330,16 +367,18 @@ def parse_polars_expression(expression: str, columns: list[str], current_cidx: i
330
367
 
331
368
  Replaces column references with Polars col() expressions:
332
369
  - $_ - Current selected column
333
- - $# - Row index (1-based, requires '^__ridx__^' column to be present)
370
+ - $# - Row index (1-based)
334
371
  - $1, $2, etc. - Column index (1-based)
335
372
  - $col_name - Column name (valid identifier starting with _ or letter)
373
+ - $`col name` - Column name with spaces (backtick quoted)
336
374
 
337
375
  Examples:
338
376
  - "$_ > 50" -> "pl.col('current_col') > 50"
339
- - "$# > 10" -> "pl.col('^__ridx__^') > 10"
377
+ - "$# > 10" -> "pl.col('^_RID_^') > 10"
340
378
  - "$1 > 50" -> "pl.col('col0') > 50"
341
379
  - "$name == 'Alex'" -> "pl.col('name') == 'Alex'"
342
380
  - "$age < $salary" -> "pl.col('age') < pl.col('salary')"
381
+ - "$`product id` > 100" -> "pl.col('product id') > 100"
343
382
 
344
383
  Args:
345
384
  expression: The input expression as a string.
@@ -368,7 +407,10 @@ def parse_polars_expression(expression: str, columns: list[str], current_cidx: i
368
407
  if isinstance(part, pl.Expr):
369
408
  col = part.meta.output_name()
370
409
 
371
- result.append(f"pl.col('{col}')")
410
+ if col == RID: # Convert to 1-based
411
+ result.append(f"(pl.col('{col}') + 1)")
412
+ else:
413
+ result.append(f"pl.col('{col}')")
372
414
  else:
373
415
  result.append(part)
374
416
 
@@ -705,3 +747,29 @@ async def sleep_async(seconds: float) -> None:
705
747
  import asyncio
706
748
 
707
749
  await asyncio.sleep(seconds)
750
+
751
+
752
+ def round_to_nearest_hundreds(num: int, N: int = 100) -> tuple[int, int]:
753
+ """Round a number to the nearest hundred boundaries.
754
+
755
+ Given a number, return a tuple of the two closest hundreds that bracket it.
756
+
757
+ Args:
758
+ num: The number to round.
759
+
760
+ Returns:
761
+ A tuple (lower_hundred, upper_hundred) where:
762
+ - lower_hundred is the largest multiple of 100 <= num
763
+ - upper_hundred is the smallest multiple of 100 > num
764
+
765
+ Examples:
766
+ >>> round_to_nearest_hundreds(0)
767
+ (0, 100)
768
+ >>> round_to_nearest_hundreds(150)
769
+ (100, 200)
770
+ >>> round_to_nearest_hundreds(200)
771
+ (200, 300)
772
+ """
773
+ lower = (num // N) * N
774
+ upper = lower + N
775
+ return (lower, upper)
@@ -74,9 +74,6 @@ class DataFrameHelpPanel(Widget):
74
74
 
75
75
  Initializes the help panel by setting up a watcher for focused widget changes
76
76
  to dynamically update help text based on which widget has focus.
77
-
78
- Returns:
79
- None
80
77
  """
81
78
 
82
79
  # def update_help(focused_widget: Widget | None):