xulbux 1.6.9__py3-none-any.whl → 1.7.1__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.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/xx_console.py CHANGED
@@ -8,10 +8,10 @@ For more detailed information about formatting codes, see the the `xx_format_cod
8
8
  from ._consts_ import COLOR, CHARS
9
9
  from .xx_format_codes import FormatCodes, _COMPILED
10
10
  from .xx_string import String
11
- from .xx_color import Color, rgba, hexa
11
+ from .xx_color import Color, Rgba, Hexa
12
12
 
13
13
  from prompt_toolkit.key_binding.key_bindings import KeyBindings
14
- from typing import Optional, Any
14
+ from typing import Optional, Literal, Mapping, Any, cast
15
15
  import prompt_toolkit as _prompt_toolkit
16
16
  import pyperclip as _pyperclip
17
17
  import keyboard as _keyboard
@@ -48,25 +48,31 @@ class _ConsoleUser:
48
48
 
49
49
 
50
50
  class ArgResult:
51
- """Exists: if the argument was found or not\n
52
- Value: the value from behind the found argument"""
51
+ """Represents the result of a parsed command-line argument and contains the following attributes:
52
+ - `exists` -⠀if the argument was found or not
53
+ - `value` -⠀the value given with the found argument\n
54
+ --------------------------------------------------------------------------------------------------------
55
+ When the `ArgResult` instance is accessed as a boolean it will correspond to the `exists` attribute."""
53
56
 
54
57
  def __init__(self, exists: bool, value: Any):
55
- self.exists = exists
56
- self.value = value
58
+ self.exists: bool = exists
59
+ self.value: Any = value
57
60
 
58
61
  def __bool__(self):
59
62
  return self.exists
60
63
 
61
64
 
62
65
  class Args:
63
- """Stores found command arguments under their aliases with their results."""
66
+ """Container for parsed command-line arguments, allowing attribute-style access.
67
+ For example, if an argument `foo` was parsed, it can be accessed via `args.foo`.
68
+ Each such attribute (e.g. `args.foo`) is an instance of `ArgResult`."""
64
69
 
65
- def __init__(self, **kwargs):
66
- for key, value in kwargs.items():
67
- if not key.isidentifier():
68
- raise TypeError(f"Argument alias '{key}' is invalid. It must be a valid Python variable name.")
69
- setattr(self, key, ArgResult(**value))
70
+ def __init__(self, **kwargs: dict[str, Any]):
71
+ for alias_name, data_dict in kwargs.items():
72
+ if not alias_name.isidentifier():
73
+ raise TypeError(f"Argument alias '{alias_name}' is invalid. It must be a valid Python variable name.")
74
+ arg_result_instance = ArgResult(exists=data_dict["exists"], value=data_dict["value"])
75
+ setattr(self, alias_name, arg_result_instance)
70
76
 
71
77
  def __len__(self):
72
78
  return len(vars(self))
@@ -74,6 +80,9 @@ class Args:
74
80
  def __contains__(self, key):
75
81
  return hasattr(self, key)
76
82
 
83
+ def __getattr__(self, name: str) -> ArgResult:
84
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
85
+
77
86
  def __getitem__(self, key):
78
87
  if isinstance(key, int):
79
88
  return list(self.__iter__())[key]
@@ -103,19 +112,18 @@ class Args:
103
112
 
104
113
  class Console:
105
114
 
106
- w: int = _ConsoleWidth()
115
+ w: int = _ConsoleWidth() # type: ignore[assignment]
107
116
  """The width of the console in characters."""
108
- h: int = _ConsoleHeight()
117
+ h: int = _ConsoleHeight() # type: ignore[assignment]
109
118
  """The height of the console in lines."""
110
- wh: tuple[int, int] = _ConsoleSize()
111
- """A tuple with the width and height of
112
- the console in characters and lines."""
113
- usr: str = _ConsoleUser()
119
+ wh: tuple[int, int] = _ConsoleSize() # type: ignore[assignment]
120
+ """A tuple with the width and height of the console in characters and lines."""
121
+ usr: str = _ConsoleUser() # type: ignore[assignment]
114
122
  """The name of the current user."""
115
123
 
116
124
  @staticmethod
117
125
  def get_args(
118
- find_args: dict[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]],
126
+ find_args: Mapping[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]],
119
127
  allow_spaces: bool = False
120
128
  ) -> Args:
121
129
  """Will search for the specified arguments in the command line
@@ -249,8 +257,8 @@ class Console:
249
257
  format_linebreaks: bool = True,
250
258
  start: str = "",
251
259
  end: str = "\n",
252
- title_bg_color: hexa | rgba = None,
253
- default_color: hexa | rgba = None,
260
+ title_bg_color: Optional[Rgba | Hexa] = None,
261
+ default_color: Optional[Rgba | Hexa] = None,
254
262
  _console_tabsize: int = 8,
255
263
  ) -> None:
256
264
  """Will print a formatted log message:
@@ -267,12 +275,20 @@ class Console:
267
275
  information about formatting codes, see `xx_format_codes` module documentation."""
268
276
  title = "" if title is None else title.strip().upper()
269
277
  title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
270
- title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
278
+ if title_bg_color is not None and Color.is_valid(title_bg_color):
279
+ title_bg_color = Color.to_hexa(title_bg_color)
280
+ title_color = Color.text_color_for_on_bg(title_bg_color)
281
+ else:
282
+ title_color = "_color" if title_bg_color is None else "#000"
271
283
  if format_linebreaks:
272
284
  clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
273
285
  prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
274
- prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
275
- prompt = f"\n{' ' * title_len}\t".join(Console.__add_back_removed_parts(list(prompt_lst), removals))
286
+ prompt_lst = (
287
+ item for lst in prompt_lst for item in ([""] if lst == [] else (lst if isinstance(lst, list) else [lst]))
288
+ )
289
+ prompt = f"\n{' ' * title_len}\t".join(
290
+ Console.__add_back_removed_parts(list(prompt_lst), cast(tuple[tuple[int, str], ...], removals))
291
+ )
276
292
  else:
277
293
  prompt = str(prompt)
278
294
  if title == "":
@@ -328,8 +344,8 @@ class Console:
328
344
  format_linebreaks: bool = True,
329
345
  start: str = "",
330
346
  end: str = "\n",
331
- title_bg_color: hexa | rgba = COLOR.yellow,
332
- default_color: hexa | rgba = COLOR.text,
347
+ title_bg_color: Rgba | Hexa = COLOR.yellow,
348
+ default_color: Rgba | Hexa = COLOR.text,
333
349
  pause: bool = False,
334
350
  exit: bool = False,
335
351
  ) -> None:
@@ -346,8 +362,8 @@ class Console:
346
362
  format_linebreaks: bool = True,
347
363
  start: str = "",
348
364
  end: str = "\n",
349
- title_bg_color: hexa | rgba = COLOR.blue,
350
- default_color: hexa | rgba = COLOR.text,
365
+ title_bg_color: Rgba | Hexa = COLOR.blue,
366
+ default_color: Rgba | Hexa = COLOR.text,
351
367
  pause: bool = False,
352
368
  exit: bool = False,
353
369
  ) -> None:
@@ -362,8 +378,8 @@ class Console:
362
378
  format_linebreaks: bool = True,
363
379
  start: str = "",
364
380
  end: str = "\n",
365
- title_bg_color: hexa | rgba = COLOR.teal,
366
- default_color: hexa | rgba = COLOR.text,
381
+ title_bg_color: Rgba | Hexa = COLOR.teal,
382
+ default_color: Rgba | Hexa = COLOR.text,
367
383
  pause: bool = False,
368
384
  exit: bool = False,
369
385
  ) -> None:
@@ -378,8 +394,8 @@ class Console:
378
394
  format_linebreaks: bool = True,
379
395
  start: str = "",
380
396
  end: str = "\n",
381
- title_bg_color: hexa | rgba = COLOR.orange,
382
- default_color: hexa | rgba = COLOR.text,
397
+ title_bg_color: Rgba | Hexa = COLOR.orange,
398
+ default_color: Rgba | Hexa = COLOR.text,
383
399
  pause: bool = False,
384
400
  exit: bool = False,
385
401
  ) -> None:
@@ -394,8 +410,8 @@ class Console:
394
410
  format_linebreaks: bool = True,
395
411
  start: str = "",
396
412
  end: str = "\n",
397
- title_bg_color: hexa | rgba = COLOR.red,
398
- default_color: hexa | rgba = COLOR.text,
413
+ title_bg_color: Rgba | Hexa = COLOR.red,
414
+ default_color: Rgba | Hexa = COLOR.text,
399
415
  pause: bool = False,
400
416
  exit: bool = True,
401
417
  reset_ansi=True,
@@ -411,8 +427,8 @@ class Console:
411
427
  format_linebreaks: bool = True,
412
428
  start: str = "",
413
429
  end: str = "\n",
414
- title_bg_color: hexa | rgba = COLOR.magenta,
415
- default_color: hexa | rgba = COLOR.text,
430
+ title_bg_color: Rgba | Hexa = COLOR.magenta,
431
+ default_color: Rgba | Hexa = COLOR.text,
416
432
  pause: bool = False,
417
433
  exit: bool = True,
418
434
  reset_ansi=True,
@@ -423,18 +439,18 @@ class Console:
423
439
  Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
424
440
 
425
441
  @staticmethod
426
- def log_box(
442
+ def log_box_filled(
427
443
  *values: object,
428
444
  start: str = "",
429
445
  end: str = "\n",
430
- box_bg_color: str | hexa | rgba = "green",
431
- default_color: hexa | rgba = "#000",
446
+ box_bg_color: Rgba | Hexa = "green",
447
+ default_color: Rgba | Hexa = "#000",
432
448
  w_padding: int = 2,
433
449
  w_full: bool = False,
434
450
  ) -> None:
435
- """Will print a box, containing a formatted log message:
451
+ """Will print a box with a colored background, containing a formatted log message:
436
452
  - `*values` -⠀the box content (each value is on a new line)
437
- - `start` -⠀something to print before the log box is printed
453
+ - `start` -⠀something to print before the log box is printed (e.g. `\\n`)
438
454
  - `end` -⠀something to print after the log box is printed (e.g. `\\n`)
439
455
  - `box_bg_color` -⠀the background color of the box
440
456
  - `default_color` -⠀the default text color of the `*values`
@@ -443,30 +459,107 @@ class Console:
443
459
  -----------------------------------------------------------------------------------
444
460
  The box content can be formatted with special formatting codes. For more detailed
445
461
  information about formatting codes, see `xx_format_codes` module documentation."""
446
- lines = [line.rstrip() for val in values for line in val.splitlines()]
447
- unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
448
- max_line_len = max(len(line) for line in unfmt_lines)
462
+ lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color)
449
463
  pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
464
+ if box_bg_color is not None and Color.is_valid(box_bg_color):
465
+ box_bg_color = Color.to_hexa(box_bg_color)
450
466
  lines = [
451
467
  f"[bg:{box_bg_color}]{' ' * w_padding}{line}" + " " *
452
- ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + "[_bg]" for line, unfmt in zip(lines, unfmt_lines)
468
+ ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + "[*]" for line, unfmt in zip(lines, unfmt_lines)
453
469
  ]
454
470
  pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
455
471
  FormatCodes.print(
456
- f"{start}[bg:{box_bg_color}]{pady}[_bg]\n"
472
+ f"{start}[bg:{box_bg_color}]{pady}[*]\n"
457
473
  + _COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", "\n".join(lines))
458
- + f"\n[bg:{box_bg_color}]{pady}[_bg]",
474
+ + f"\n[bg:{box_bg_color}]{pady}[_]",
475
+ default_color=default_color or "#000",
476
+ sep="\n",
477
+ end=end,
478
+ )
479
+
480
+ @staticmethod
481
+ def log_box_bordered(
482
+ *values: object,
483
+ start: str = "",
484
+ end: str = "\n",
485
+ border_type: Literal["standard", "rounded", "strong", "double"] = "rounded",
486
+ border_style: str | Rgba | Hexa = f"dim|{COLOR.gray}",
487
+ default_color: Optional[Rgba | Hexa] = None,
488
+ w_padding: int = 1,
489
+ w_full: bool = False,
490
+ _border_chars: Optional[tuple[str, str, str, str, str, str, str, str]] = None,
491
+ ) -> None:
492
+ """Will print a bordered box, containing a formatted log message:
493
+ - `*values` -⠀the box content (each value is on a new line)
494
+ - `start` -⠀something to print before the log box is printed (e.g. `\\n`)
495
+ - `end` -⠀something to print after the log box is printed (e.g. `\\n`)
496
+ - `border_type` -⠀one of the predefined border character sets
497
+ - `default_color` -⠀the default text color of the `*values`
498
+ - `w_padding` -⠀the horizontal padding (in chars) to the box content
499
+ - `w_full` -⠀whether to make the box be the full console width or not
500
+ - `_border_chars` -⠀define your own border characters set (overwrites `border_type`)\n
501
+ ---------------------------------------------------------------------------------------
502
+ The box content can be formatted with special formatting codes. For more detailed
503
+ information about formatting codes, see `xx_format_codes` module documentation.\n
504
+ ---------------------------------------------------------------------------------------
505
+ The `border_type` can be one of the following:
506
+ - `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│')`
507
+ - `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│')`
508
+ - `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃')`
509
+ - `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║')`\n
510
+ The order of the characters is always:
511
+ 1. top-left corner
512
+ 2. top border
513
+ 3. top-right corner
514
+ 4. right border
515
+ 5. bottom-right corner
516
+ 6. bottom border
517
+ 7. bottom-left corner
518
+ 8. left border"""
519
+ borders = {
520
+ "standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│'),
521
+ "rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│'),
522
+ "strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃'),
523
+ "double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║'),
524
+ }
525
+ border_chars = borders.get(border_type, borders["standard"]) if _border_chars is None else _border_chars
526
+ lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color)
527
+ print(unfmt_lines)
528
+ pad_w_full = (Console.w - (max_line_len + (2 * w_padding)) - (len(border_chars[1] * 2))) if w_full else 0
529
+ if border_style is not None and Color.is_valid(border_style):
530
+ border_style = Color.to_hexa(border_style)
531
+ border_l = f"[{border_style}]{border_chars[7]}[*]"
532
+ border_r = f"[{border_style}]{border_chars[3]}[_]"
533
+ lines = [
534
+ f"{border_l}{' ' * w_padding}{line}[_]" + " " * ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + border_r
535
+ for line, unfmt in zip(lines, unfmt_lines)
536
+ ]
537
+ border_t = f"[{border_style}]{border_chars[0]}{border_chars[1] * (Console.w - (len(border_chars[1] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[2]}[_]"
538
+ border_b = f"[{border_style}]{border_chars[6]}{border_chars[5] * (Console.w - (len(border_chars[1] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[4]}[_]"
539
+ FormatCodes.print(
540
+ f"{start}{border_t}[_]\n" + "\n".join(lines) + f"\n{border_b}[_]",
459
541
  default_color=default_color,
460
542
  sep="\n",
461
543
  end=end,
462
544
  )
463
545
 
546
+ @staticmethod
547
+ def __prepare_log_box(
548
+ values: tuple[object, ...],
549
+ default_color: Optional[Rgba | Hexa] = None,
550
+ ) -> tuple[list[str], list[tuple[str, tuple[tuple[int, str], ...]]], int]:
551
+ """Prepares the log box content and returns it along with the max line length."""
552
+ lines = [line for val in values for line in str(val).splitlines()]
553
+ unfmt_lines = [FormatCodes.remove_formatting(line, default_color) for line in lines]
554
+ max_line_len = max(len(line) for line in unfmt_lines)
555
+ return lines, cast(list[tuple[str, tuple[tuple[int, str], ...]]], unfmt_lines), max_line_len
556
+
464
557
  @staticmethod
465
558
  def confirm(
466
559
  prompt: object = "Do you want to continue?",
467
560
  start="",
468
561
  end="\n",
469
- default_color: hexa | rgba = COLOR.cyan,
562
+ default_color: Rgba | Hexa = COLOR.cyan,
470
563
  default_is_yes: bool = True,
471
564
  ) -> bool:
472
565
  """Ask a yes/no question.\n
@@ -488,9 +581,9 @@ class Console:
488
581
  prompt: object = "",
489
582
  start="",
490
583
  end="\n",
491
- default_color: hexa | rgba = COLOR.cyan,
584
+ default_color: Rgba | Hexa = COLOR.cyan,
492
585
  show_keybindings=True,
493
- input_prefix=" ",
586
+ input_prefix=" ",
494
587
  reset_ansi=True,
495
588
  ) -> str:
496
589
  """An input where users can input (and paste) text over multiple lines.\n
@@ -511,7 +604,7 @@ class Console:
511
604
  def _(event):
512
605
  event.app.exit(result=event.app.current_buffer.document.text)
513
606
 
514
- FormatCodes.print(start + prompt, default_color=default_color)
607
+ FormatCodes.print(start + str(prompt), default_color=default_color)
515
608
  if show_keybindings:
516
609
  FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
517
610
  input_string = _prompt_toolkit.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
@@ -523,11 +616,11 @@ class Console:
523
616
  prompt: object = "",
524
617
  start="",
525
618
  end="\n",
526
- default_color: hexa | rgba = COLOR.cyan,
527
- allowed_chars: str = CHARS.all,
528
- min_len: int = None,
529
- max_len: int = None,
530
- mask_char: str = None,
619
+ default_color: Rgba | Hexa = COLOR.cyan,
620
+ allowed_chars: str = CHARS.all, # type: ignore[assignment]
621
+ min_len: Optional[int] = None,
622
+ max_len: Optional[int] = None,
623
+ mask_char: Optional[str] = None,
531
624
  reset_ansi: bool = True,
532
625
  ) -> Optional[str]:
533
626
  """Acts like a standard Python `input()` with the advantage, that you can specify:
@@ -538,7 +631,7 @@ class Console:
538
631
  ---------------------------------------------------------------------------------------
539
632
  The input can be formatted with special formatting codes. For more detailed
540
633
  information about formatting codes, see the `xx_format_codes` module documentation."""
541
- FormatCodes.print(start + prompt, default_color=default_color, end="")
634
+ FormatCodes.print(start + str(prompt), default_color=default_color, end="")
542
635
  result = ""
543
636
  select_all = False
544
637
  last_line_count = 1
@@ -594,7 +687,8 @@ class Console:
594
687
 
595
688
  def handle_character_input():
596
689
  nonlocal result
597
- if (allowed_chars == CHARS.all or event.name in allowed_chars) and (max_len is None or len(result) < max_len):
690
+ if event.name is not None and ((allowed_chars == CHARS.all or event.name in allowed_chars) and
691
+ (max_len is None or len(result) < max_len)):
598
692
  result += event.name
599
693
  update_display(Console.w)
600
694
 
@@ -615,7 +709,7 @@ class Console:
615
709
  return None
616
710
  elif event.name == "space":
617
711
  handle_character_input()
618
- elif len(event.name) == 1:
712
+ elif event.name is not None and len(event.name) == 1:
619
713
  handle_character_input()
620
714
  else:
621
715
  select_all = False
@@ -626,12 +720,22 @@ class Console:
626
720
  prompt: object = "Password: ",
627
721
  start="",
628
722
  end="\n",
629
- default_color: hexa | rgba = COLOR.cyan,
723
+ default_color: Rgba | Hexa = COLOR.cyan,
630
724
  allowed_chars: str = CHARS.standard_ascii,
631
- min_len: int = None,
632
- max_len: int = None,
725
+ min_len: Optional[int] = None,
726
+ max_len: Optional[int] = None,
633
727
  reset_ansi: bool = True,
634
- ) -> str:
728
+ ) -> Optional[str]:
635
729
  """Password input (preset for `Console.restricted_input()`)
636
730
  that always masks the entered characters with asterisks."""
637
- return Console.restricted_input(prompt, start, end, default_color, allowed_chars, min_len, max_len, "*", reset_ansi)
731
+ return Console.restricted_input(
732
+ prompt=prompt,
733
+ start=start,
734
+ end=end,
735
+ default_color=default_color,
736
+ allowed_chars=allowed_chars,
737
+ min_len=min_len,
738
+ max_len=max_len,
739
+ mask_char="*",
740
+ reset_ansi=reset_ansi,
741
+ )
xulbux/xx_data.py CHANGED
@@ -166,7 +166,7 @@ class Data:
166
166
 
167
167
  def process_string(s: str) -> Optional[str]:
168
168
  if comment_end:
169
- match = pattern.match(s)
169
+ match = pattern.match(s) # type: ignore[unbound]
170
170
  if match:
171
171
  start, end = match.group(1).strip(), match.group(2).strip()
172
172
  return f"{start}{comment_sep if start and end else ''}{end}" or None
@@ -223,7 +223,7 @@ class Data:
223
223
  return True
224
224
  if type(d1) is not type(d2):
225
225
  return False
226
- if isinstance(d1, dict):
226
+ if isinstance(d1, dict) and isinstance(d2, dict):
227
227
  if set(d1.keys()) != set(d2.keys()):
228
228
  return False
229
229
  return all(compare(d1[key], d2[key], ignore_paths, current_path + [key]) for key in d1)
@@ -251,7 +251,7 @@ class Data:
251
251
  comment_start: str = ">>",
252
252
  comment_end: str = "<<",
253
253
  ignore_not_found: bool = False,
254
- ) -> str | list[str]:
254
+ ) -> Optional[str | list[Optional[str]]]:
255
255
  """Generates a unique ID based on the path to a specific value within a nested data structure.\n
256
256
  -------------------------------------------------------------------------------------------------
257
257
  The `data` parameter is the list, tuple, or dictionary, which the id should be generated for.\n
@@ -355,12 +355,12 @@ class Data:
355
355
  return get_nested(data, Data.__sep_path_id(path_id), get_key)
356
356
 
357
357
  @staticmethod
358
- def set_value_by_path_id(data: DataStructure, update_values: dict[str, Any]) -> list | tuple | dict:
358
+ def set_value_by_path_id(data: DataStructure, update_values: dict[str, Any]) -> DataStructure:
359
359
  """Updates the value/s from `update_values` in the `data`.\n
360
360
  --------------------------------------------------------------------------------
361
361
  Input a list, tuple or dict as `data`, along with `update_values`, which is a
362
362
  dictionary where keys are path IDs and values are the new values to insert:
363
- { "1>": "new value", "path_id2": ["new value 1", "new value 2"], ... }
363
+ { "1>012": "new value", "1>31": ["new value 1", "new value 2"], ... }
364
364
  The path IDs should have been created using `Data.get_path_id()`.\n
365
365
  --------------------------------------------------------------------------------
366
366
  The value from path ID will be changed to the new value, as long as the
@@ -413,7 +413,8 @@ class Data:
413
413
  - `2` keeps everything collapsed (all on one line)\n
414
414
  ------------------------------------------------------------------------------
415
415
  If `as_json` is set to `True`, the output will be in valid JSON format."""
416
- if syntax_hl := _syntax_highlighting not in (None, False):
416
+ _syntax_hl = {}
417
+ if do_syntax_hl := _syntax_highlighting not in (None, False):
417
418
  if _syntax_highlighting is True:
418
419
  _syntax_highlighting = {}
419
420
  elif not isinstance(_syntax_highlighting, dict):
@@ -426,62 +427,62 @@ class Data:
426
427
  "punctuation": (f"[{COLOR.darkgray}]", "[_c]"),
427
428
  }
428
429
  _syntax_hl.update({
429
- k: [f"[{v}]", "[_]"] if k in _syntax_hl and v not in ("", None) else ["", ""]
430
+ k: (f"[{v}]", "[_]") if k in _syntax_hl and v not in ("", None) else ("", "")
430
431
  for k, v in _syntax_highlighting.items()
431
432
  })
432
433
  sep = f"{_syntax_hl['punctuation'][0]}{sep}{_syntax_hl['punctuation'][1]}"
433
434
  punct_map = {"(": ("/(", "("), **{char: char for char in "'\":)[]{}"}}
434
435
  punct = {
435
- k: ((f"{_syntax_hl['punctuation'][0]}{v[0]}{_syntax_hl['punctuation'][1]}" if syntax_hl else v[1])
436
+ k: ((f"{_syntax_hl['punctuation'][0]}{v[0]}{_syntax_hl['punctuation'][1]}" if do_syntax_hl else v[1])
436
437
  if isinstance(v, (list, tuple)) else
437
- (f"{_syntax_hl['punctuation'][0]}{v}{_syntax_hl['punctuation'][1]}" if syntax_hl else v))
438
+ (f"{_syntax_hl['punctuation'][0]}{v}{_syntax_hl['punctuation'][1]}" if do_syntax_hl else v))
438
439
  for k, v in punct_map.items()
439
440
  }
440
441
 
441
- def format_value(value: Any, current_indent: int = None) -> str:
442
+ def format_value(value: Any, current_indent: Optional[int] = None) -> str:
442
443
  if current_indent is not None and isinstance(value, dict):
443
444
  return format_dict(value, current_indent + indent)
444
445
  elif current_indent is not None and hasattr(value, "__dict__"):
445
446
  return format_dict(value.__dict__, current_indent + indent)
446
447
  elif current_indent is not None and isinstance(value, IndexIterable):
447
448
  return format_sequence(value, current_indent + indent)
448
- elif isinstance(value, (bytes, bytearray)):
449
+ elif current_indent is not None and isinstance(value, (bytes, bytearray)):
449
450
  obj_dict = Data.serialize_bytes(value)
450
451
  return (
451
452
  format_dict(obj_dict, current_indent + indent) if as_json else (
452
453
  f"{_syntax_hl['type'][0]}{(k := next(iter(obj_dict)))}{_syntax_hl['type'][1]}"
453
- + format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent) if syntax_hl else
454
+ + format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent) if do_syntax_hl else
454
455
  (k := next(iter(obj_dict)))
455
456
  + format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent)
456
457
  )
457
458
  )
458
459
  elif isinstance(value, bool):
459
460
  val = str(value).lower() if as_json else str(value)
460
- return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
461
+ return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if do_syntax_hl else val
461
462
  elif isinstance(value, (int, float)):
462
463
  val = "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
463
- return f"{_syntax_hl['number'][0]}{val}{_syntax_hl['number'][1]}" if syntax_hl else val
464
- elif isinstance(value, complex):
464
+ return f"{_syntax_hl['number'][0]}{val}{_syntax_hl['number'][1]}" if do_syntax_hl else val
465
+ elif current_indent is not None and isinstance(value, complex):
465
466
  return (
466
467
  format_value(str(value).strip("()")) if as_json else (
467
468
  f"{_syntax_hl['type'][0]}complex{_syntax_hl['type'][1]}"
468
469
  + format_sequence((value.real, value.imag), current_indent + indent)
469
- if syntax_hl else f"complex{format_sequence((value.real, value.imag), current_indent + indent)}"
470
+ if do_syntax_hl else f"complex{format_sequence((value.real, value.imag), current_indent + indent)}"
470
471
  )
471
472
  )
472
473
  elif value is None:
473
474
  val = "null" if as_json else "None"
474
- return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
475
+ return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if do_syntax_hl else val
475
476
  else:
476
477
  return ((
477
478
  punct['"'] + _syntax_hl["str"][0] + String.escape(str(value), '"') + _syntax_hl["str"][1]
478
- + punct['"'] if syntax_hl else punct['"'] + String.escape(str(value), '"') + punct['"']
479
+ + punct['"'] if do_syntax_hl else punct['"'] + String.escape(str(value), '"') + punct['"']
479
480
  ) if as_json else (
480
481
  punct["'"] + _syntax_hl["str"][0] + String.escape(str(value), "'") + _syntax_hl["str"][1]
481
- + punct["'"] if syntax_hl else punct["'"] + String.escape(str(value), "'") + punct["'"]
482
+ + punct["'"] if do_syntax_hl else punct["'"] + String.escape(str(value), "'") + punct["'"]
482
483
  ))
483
484
 
484
- def should_expand(seq: list | tuple | dict) -> bool:
485
+ def should_expand(seq: IndexIterable) -> bool:
485
486
  if compactness == 0:
486
487
  return True
487
488
  if compactness == 2:
@@ -500,7 +501,7 @@ class Data:
500
501
  + sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
501
502
  for k, v in d.items()) + punct["}"]
502
503
  )
503
- if not should_expand(d.values()):
504
+ if not should_expand(list(d.values())):
504
505
  return (
505
506
  punct["{"]
506
507
  + sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
xulbux/xx_env_path.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from .xx_path import Path
2
2
 
3
+ from typing import Optional
3
4
  import sys as _sys
4
5
  import os as _os
5
6
 
@@ -13,7 +14,7 @@ class EnvPath:
13
14
  return paths.split(_os.pathsep) if as_list else paths
14
15
 
15
16
  @staticmethod
16
- def has_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> bool:
17
+ def has_path(path: Optional[str] = None, cwd: bool = False, base_dir: bool = False) -> bool:
17
18
  """Check if a path is present in the PATH environment variable."""
18
19
  if cwd:
19
20
  path = _os.getcwd()
@@ -25,21 +26,21 @@ class EnvPath:
25
26
  return _os.path.normpath(path) in [_os.path.normpath(p) for p in paths]
26
27
 
27
28
  @staticmethod
28
- def add_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> None:
29
+ def add_path(path: Optional[str] = None, cwd: bool = False, base_dir: bool = False) -> None:
29
30
  """Add a path to the PATH environment variable."""
30
31
  path = EnvPath.__get(path, cwd, base_dir)
31
32
  if not EnvPath.has_path(path):
32
33
  EnvPath.__persistent(path, add=True)
33
34
 
34
35
  @staticmethod
35
- def remove_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> None:
36
+ def remove_path(path: Optional[str] = None, cwd: bool = False, base_dir: bool = False) -> None:
36
37
  """Remove a path from the PATH environment variable."""
37
38
  path = EnvPath.__get(path, cwd, base_dir)
38
39
  if EnvPath.has_path(path):
39
40
  EnvPath.__persistent(path, remove=True)
40
41
 
41
42
  @staticmethod
42
- def __get(path: str = None, cwd: bool = False, base_dir: bool = False) -> list:
43
+ def __get(path: Optional[str] = None, cwd: bool = False, base_dir: bool = False) -> str:
43
44
  """Get and/or normalize the paths.\n
44
45
  ------------------------------------------------------------------------------------
45
46
  Raise an error if no path is provided and neither `cwd` or `base_dir` is `True`."""
@@ -56,7 +57,7 @@ class EnvPath:
56
57
  """Add or remove a path from PATH persistently across sessions as well as the current session."""
57
58
  if add == remove:
58
59
  raise ValueError("Either add or remove must be True, but not both.")
59
- current_paths = EnvPath.paths(as_list=True)
60
+ current_paths = list(EnvPath.paths(as_list=True))
60
61
  path = _os.path.normpath(path)
61
62
  if remove:
62
63
  current_paths = [p for p in current_paths if _os.path.normpath(p) != _os.path.normpath(path)]
xulbux/xx_file.py CHANGED
@@ -19,8 +19,8 @@ class File:
19
19
  """Rename the extension of a file.\n
20
20
  --------------------------------------------------------------------------
21
21
  If `full_extension` is true, everything after the first dot in the
22
- filename will be treated as the extension to replace. Otherwise, only the
23
- part after the last dot is replaced.\n
22
+ filename will be treated as the extension to replace (e.g. `.tar.gz`).
23
+ Otherwise, only the part after the last dot is replaced (e.g. `.gz`).\n
24
24
  If the `camel_case_filename` parameter is true, the filename will be made
25
25
  CamelCase in addition to changing the files extension."""
26
26
  normalized_file = _os.path.normpath(file)