xulbux 1.8.2__py3-none-any.whl → 1.8.3__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/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.8.2"
1
+ __version__ = "1.8.3"
2
2
 
3
3
  __author__ = "XulbuX"
4
4
  __email__ = "xulbux.real@gmail.com"
xulbux/cli/help.py CHANGED
@@ -20,8 +20,8 @@ def get_latest_version() -> Optional[str]:
20
20
 
21
21
  def is_latest_version() -> Optional[bool]:
22
22
  try:
23
- latest = get_latest_version()
24
- if latest in ("", None): return None
23
+ if (latest := get_latest_version()) in ("", None):
24
+ return None
25
25
  latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
26
26
  installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
27
27
  return latest_v_parts <= installed_v_parts
xulbux/console.py CHANGED
@@ -10,7 +10,7 @@ from .format_codes import FormatCodes, _COMPILED as _FC_COMPILED
10
10
  from .string import String
11
11
  from .color import Color, Rgba, Hexa
12
12
 
13
- from typing import Generator, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, cast
13
+ from typing import Generator, TypedDict, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, overload, cast, Protocol
14
14
  from prompt_toolkit.key_binding import KeyPressEvent, KeyBindings
15
15
  from prompt_toolkit.validation import ValidationError, Validator
16
16
  from prompt_toolkit.styles import Style
@@ -27,6 +27,10 @@ import io as _io
27
27
 
28
28
 
29
29
  _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
30
+ "hr": _re.compile(r"(?i)\{hr\}"),
31
+ "hr_no_nl": _re.compile(r"(?i)(?<!\n){hr}(?!\n)"),
32
+ "hr_r_nl": _re.compile(r"(?i)(?<!\n){hr}(?=\n)"),
33
+ "hr_l_nl": _re.compile(r"(?i)(?<=\n){hr}(?!\n)"),
30
34
  "label": _re.compile(r"(?i)\{(?:label|l)\}"),
31
35
  "bar": _re.compile(r"(?i)\{(?:bar|b)\}"),
32
36
  "current": _re.compile(r"(?i)\{(?:current|c)\}"),
@@ -69,6 +73,11 @@ class _ConsoleUser:
69
73
  return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
70
74
 
71
75
 
76
+ class _ArgConfigWithDefault(TypedDict):
77
+ flags: list[str] | tuple[str, ...]
78
+ default: Any
79
+
80
+
72
81
  class ArgResult:
73
82
  """Represents the result of a parsed command-line argument and contains the following attributes:
74
83
  - `exists` -⠀if the argument was found or not
@@ -146,8 +155,10 @@ class Console:
146
155
 
147
156
  @staticmethod
148
157
  def get_args(
149
- find_args: Mapping[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]
150
- | Literal["before", "after"]],
158
+ find_args: Mapping[
159
+ str,
160
+ list[str] | tuple[str, ...] | _ArgConfigWithDefault | Literal["before", "after"],
161
+ ],
151
162
  allow_spaces: bool = False
152
163
  ) -> Args:
153
164
  """Will search for the specified arguments in the command line
@@ -241,8 +252,11 @@ class Console:
241
252
  elif isinstance(config, dict):
242
253
  if "flags" not in config:
243
254
  raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
244
- flags = config["flags"]
245
- default_value = config.get("default")
255
+ if "default" not in config:
256
+ raise ValueError(
257
+ f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'default' key. Use a simple list/tuple if no default value is needed."
258
+ )
259
+ flags, default_value = config["flags"], config["default"]
246
260
  if not isinstance(flags, (list, tuple)):
247
261
  raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
248
262
  results[alias] = {"exists": False, "value": default_value}
@@ -269,14 +283,14 @@ class Console:
269
283
  if first_flag_pos is None:
270
284
  first_flag_pos = i
271
285
  # CHECK IF THIS FLAG HAS A VALUE FOLLOWING IT
272
- flag_has_value = (i + 1 < args_len and not args[i + 1].startswith("-") and args[i + 1] not in arg_lookup)
286
+ flag_has_value = (i + 1 < args_len and args[i + 1] not in arg_lookup)
273
287
  if flag_has_value:
274
288
  if not allow_spaces:
275
289
  last_flag_with_value_pos = i + 1
276
290
  else:
277
291
  # FIND THE END OF THE MULTI-WORD VALUE
278
292
  j = i + 1
279
- while j < args_len and not args[j].startswith("-") and args[j] not in arg_lookup:
293
+ while j < args_len and args[j] not in arg_lookup:
280
294
  j += 1
281
295
  last_flag_with_value_pos = j - 1
282
296
 
@@ -286,7 +300,7 @@ class Console:
286
300
  before_args = []
287
301
  end_pos = first_flag_pos if first_flag_pos is not None else args_len
288
302
  for i in range(end_pos):
289
- if not args[i].startswith("-"):
303
+ if args[i] not in arg_lookup:
290
304
  before_args.append(String.to_type(args[i]))
291
305
  if before_args:
292
306
  results[alias]["value"] = before_args
@@ -300,7 +314,7 @@ class Console:
300
314
  if alias:
301
315
  results[alias]["exists"] = True
302
316
  value_found_after_flag = False
303
- if i + 1 < args_len and not args[i + 1].startswith("-"):
317
+ if i + 1 < args_len and args[i + 1] not in arg_lookup:
304
318
  if not allow_spaces:
305
319
  results[alias]["value"] = String.to_type(args[i + 1])
306
320
  i += 1
@@ -308,7 +322,7 @@ class Console:
308
322
  else:
309
323
  value_parts = []
310
324
  j = i + 1
311
- while j < args_len and not args[j].startswith("-"):
325
+ while j < args_len and args[j] not in arg_lookup:
312
326
  value_parts.append(args[j])
313
327
  j += 1
314
328
  if value_parts:
@@ -316,7 +330,7 @@ class Console:
316
330
  i = j - 1
317
331
  value_found_after_flag = True
318
332
  if not value_found_after_flag:
319
- results[alias]["value"] = True
333
+ results[alias]["value"] = None
320
334
  i += 1
321
335
 
322
336
  # COLLECT "after" POSITIONAL ARGUMENTS
@@ -335,7 +349,7 @@ class Console:
335
349
  start_pos = last_flag_pos + 1
336
350
 
337
351
  for i in range(start_pos, args_len):
338
- if not args[i].startswith("-") and args[i] not in arg_lookup:
352
+ if args[i] not in arg_lookup:
339
353
  after_args.append(String.to_type(args[i]))
340
354
 
341
355
  if after_args:
@@ -346,9 +360,9 @@ class Console:
346
360
 
347
361
  @staticmethod
348
362
  def pause_exit(
363
+ prompt: object = "",
349
364
  pause: bool = True,
350
365
  exit: bool = False,
351
- prompt: object = "",
352
366
  exit_code: int = 0,
353
367
  reset_ansi: bool = False,
354
368
  ) -> None:
@@ -473,13 +487,15 @@ class Console:
473
487
  default_color: Optional[Rgba | Hexa] = None,
474
488
  pause: bool = False,
475
489
  exit: bool = False,
490
+ exit_code: int = 0,
491
+ reset_ansi: bool = True,
476
492
  ) -> None:
477
493
  """A preset for `log()`: `DEBUG` log message with the options to pause
478
494
  at the message and exit the program after the message was printed.
479
495
  If `active` is false, no debug message will be printed."""
480
496
  if active:
481
497
  Console.log("DEBUG", prompt, format_linebreaks, start, end, COLOR.YELLOW, default_color)
482
- Console.pause_exit(pause, exit)
498
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
483
499
 
484
500
  @staticmethod
485
501
  def info(
@@ -490,11 +506,13 @@ class Console:
490
506
  default_color: Optional[Rgba | Hexa] = None,
491
507
  pause: bool = False,
492
508
  exit: bool = False,
509
+ exit_code: int = 0,
510
+ reset_ansi: bool = True,
493
511
  ) -> None:
494
512
  """A preset for `log()`: `INFO` log message with the options to pause
495
513
  at the message and exit the program after the message was printed."""
496
514
  Console.log("INFO", prompt, format_linebreaks, start, end, COLOR.BLUE, default_color)
497
- Console.pause_exit(pause, exit)
515
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
498
516
 
499
517
  @staticmethod
500
518
  def done(
@@ -505,11 +523,13 @@ class Console:
505
523
  default_color: Optional[Rgba | Hexa] = None,
506
524
  pause: bool = False,
507
525
  exit: bool = False,
526
+ exit_code: int = 0,
527
+ reset_ansi: bool = True,
508
528
  ) -> None:
509
529
  """A preset for `log()`: `DONE` log message with the options to pause
510
530
  at the message and exit the program after the message was printed."""
511
531
  Console.log("DONE", prompt, format_linebreaks, start, end, COLOR.TEAL, default_color)
512
- Console.pause_exit(pause, exit)
532
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
513
533
 
514
534
  @staticmethod
515
535
  def warn(
@@ -520,11 +540,13 @@ class Console:
520
540
  default_color: Optional[Rgba | Hexa] = None,
521
541
  pause: bool = False,
522
542
  exit: bool = False,
543
+ exit_code: int = 1,
544
+ reset_ansi: bool = True,
523
545
  ) -> None:
524
546
  """A preset for `log()`: `WARN` log message with the options to pause
525
547
  at the message and exit the program after the message was printed."""
526
548
  Console.log("WARN", prompt, format_linebreaks, start, end, COLOR.ORANGE, default_color)
527
- Console.pause_exit(pause, exit)
549
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
528
550
 
529
551
  @staticmethod
530
552
  def fail(
@@ -535,12 +557,13 @@ class Console:
535
557
  default_color: Optional[Rgba | Hexa] = None,
536
558
  pause: bool = False,
537
559
  exit: bool = True,
560
+ exit_code: int = 1,
538
561
  reset_ansi: bool = True,
539
562
  ) -> None:
540
563
  """A preset for `log()`: `FAIL` log message with the options to pause
541
564
  at the message and exit the program after the message was printed."""
542
565
  Console.log("FAIL", prompt, format_linebreaks, start, end, COLOR.RED, default_color)
543
- Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
566
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
544
567
 
545
568
  @staticmethod
546
569
  def exit(
@@ -551,12 +574,13 @@ class Console:
551
574
  default_color: Optional[Rgba | Hexa] = None,
552
575
  pause: bool = False,
553
576
  exit: bool = True,
577
+ exit_code: int = 0,
554
578
  reset_ansi: bool = True,
555
579
  ) -> None:
556
580
  """A preset for `log()`: `EXIT` log message with the options to pause
557
581
  at the message and exit the program after the message was printed."""
558
582
  Console.log("EXIT", prompt, format_linebreaks, start, end, COLOR.MAGENTA, default_color)
559
- Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
583
+ Console.pause_exit("", pause=pause, exit=exit, exit_code=exit_code, reset_ansi=reset_ansi)
560
584
 
561
585
  @staticmethod
562
586
  def log_box_filled(
@@ -611,7 +635,7 @@ class Console:
611
635
  w_padding: int = 1,
612
636
  w_full: bool = False,
613
637
  indent: int = 0,
614
- _border_chars: Optional[tuple[str, str, str, str, str, str, str, str]] = None,
638
+ _border_chars: Optional[tuple[str, str, str, str, str, str, str, str, str, str, str]] = None,
615
639
  ) -> None:
616
640
  """Will print a bordered box, containing a formatted log message:
617
641
  - `*values` -⠀the box content (each value is on a new line)
@@ -624,15 +648,17 @@ class Console:
624
648
  - `w_full` -⠀whether to make the box be the full console width or not
625
649
  - `indent` -⠀the indentation of the box (in chars)
626
650
  - `_border_chars` -⠀define your own border characters set (overwrites `border_type`)\n
627
- ---------------------------------------------------------------------------------------
651
+ ---------------------------------------------------------------------------------------------
652
+ You can insert horizontal rules to split the box content by using `{hr}` in the `*values`.\n
653
+ ---------------------------------------------------------------------------------------------
628
654
  The box content can be formatted with special formatting codes. For more detailed
629
655
  information about formatting codes, see `format_codes` module documentation.\n
630
- ---------------------------------------------------------------------------------------
656
+ ---------------------------------------------------------------------------------------------
631
657
  The `border_type` can be one of the following:
632
- - `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│')`
633
- - `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│')`
634
- - `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃')`
635
- - `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║')`\n
658
+ - `"standard" = ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤')`
659
+ - `"rounded" = ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤')`
660
+ - `"strong" = ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫')`
661
+ - `"double" = ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣')`\n
636
662
  The order of the characters is always:
637
663
  1. top-left corner
638
664
  2. top border
@@ -641,27 +667,31 @@ class Console:
641
667
  5. bottom-right corner
642
668
  6. bottom border
643
669
  7. bottom-left corner
644
- 8. left border"""
670
+ 8. left border
671
+ 9. left horizontal rule connector
672
+ 10. horizontal rule
673
+ 11. right horizontal rule connector"""
645
674
  borders = {
646
- "standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│'),
647
- "rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│'),
648
- "strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃'),
649
- "double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║'),
675
+ "standard": ('┌', '─', '┐', '│', '┘', '─', '└', '│', '├', '─', '┤'),
676
+ "rounded": ('╭', '─', '╮', '│', '╯', '─', '╰', '│', '├', '─', '┤'),
677
+ "strong": ('┏', '━', '┓', '┃', '┛', '━', '┗', '┃', '┣', '━', '┫'),
678
+ "double": ('╔', '═', '╗', '║', '╝', '═', '╚', '║', '╠', '═', '╣'),
650
679
  }
651
680
  border_chars = borders.get(border_type, borders["standard"]) if _border_chars is None else _border_chars
652
- lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color)
681
+ lines, unfmt_lines, max_line_len = Console.__prepare_log_box(values, default_color, has_rules=True)
653
682
  pad_w_full = (Console.w - (max_line_len + (2 * w_padding)) - (len(border_chars[1] * 2))) if w_full else 0
654
683
  if border_style is not None and Color.is_valid(border_style):
655
684
  border_style = Color.to_hexa(border_style)
656
685
  spaces_l = " " * indent
657
686
  border_l = f"[{border_style}]{border_chars[7]}[*]"
658
687
  border_r = f"[{border_style}]{border_chars[3]}[_]"
688
+ border_t = f"{spaces_l}[{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]}[_]"
689
+ border_b = f"{spaces_l}[{border_style}]{border_chars[6]}{border_chars[5] * (Console.w - (len(border_chars[5] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[4]}[_]"
690
+ h_rule = f"{spaces_l}[{border_style}]{border_chars[8]}{border_chars[9] * (Console.w - (len(border_chars[9] * 2)) if w_full else max_line_len + (2 * w_padding))}{border_chars[10]}[_]"
659
691
  lines = [
660
- f"{spaces_l}{border_l}{' ' * w_padding}{line}[_]" + " " *
692
+ h_rule if _COMPILED["hr"].match(line) else f"{spaces_l}{border_l}{' ' * w_padding}{line}[_]" + " " *
661
693
  ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + border_r for line, unfmt in zip(lines, unfmt_lines)
662
694
  ]
663
- border_t = f"{spaces_l}[{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]}[_]"
664
- border_b = f"{spaces_l}[{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]}[_]"
665
695
  FormatCodes.print(
666
696
  f"{start}{border_t}[_]\n" + "\n".join(lines) + f"\n{border_b}[_]",
667
697
  default_color=default_color,
@@ -673,9 +703,42 @@ class Console:
673
703
  def __prepare_log_box(
674
704
  values: tuple[object, ...],
675
705
  default_color: Optional[Rgba | Hexa] = None,
706
+ has_rules: bool = False,
676
707
  ) -> tuple[list[str], list[tuple[str, tuple[tuple[int, str], ...]]], int]:
677
708
  """Prepares the log box content and returns it along with the max line length."""
678
- lines = [line for val in values for line in str(val).splitlines()]
709
+ if has_rules:
710
+ lines = []
711
+ for val in values:
712
+ val_str, result_parts, current_pos = str(val), [], 0
713
+ for match in _COMPILED["hr"].finditer(val_str):
714
+ start, end = match.span()
715
+ should_split_before = start > 0 and val_str[start - 1] != '\n'
716
+ should_split_after = end < len(val_str) and val_str[end] != '\n'
717
+
718
+ if should_split_before:
719
+ if start > current_pos:
720
+ result_parts.append(val_str[current_pos:start])
721
+ if should_split_after:
722
+ result_parts.append(match.group())
723
+ current_pos = end
724
+ else:
725
+ current_pos = start
726
+ else:
727
+ if should_split_after:
728
+ result_parts.append(val_str[current_pos:end])
729
+ current_pos = end
730
+
731
+ if current_pos < len(val_str):
732
+ result_parts.append(val_str[current_pos:])
733
+
734
+ if not result_parts:
735
+ result_parts.append(val_str)
736
+
737
+ for part in result_parts:
738
+ lines.extend(part.splitlines())
739
+ else:
740
+ lines = [line for val in values for line in str(val).splitlines()]
741
+
679
742
  unfmt_lines = [FormatCodes.remove_formatting(line, default_color) for line in lines]
680
743
  max_line_len = max(len(line) for line in unfmt_lines)
681
744
  return lines, cast(list[tuple[str, tuple[tuple[int, str], ...]]], unfmt_lines), max_line_len
@@ -755,7 +818,7 @@ class Console:
755
818
  mask_char: Optional[str] = None,
756
819
  min_len: Optional[int] = None,
757
820
  max_len: Optional[int] = None,
758
- allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
821
+ allowed_chars: str = CHARS.ALL, # type: ignore[assignment]
759
822
  allow_paste: bool = True,
760
823
  validator: Optional[Callable[[str], Optional[str]]] = None,
761
824
  default_val: Optional[T] = None,
@@ -825,7 +888,8 @@ class Console:
825
888
 
826
889
  def process_insert_text(text: str) -> tuple[str, set[str]]:
827
890
  removed_chars = set()
828
- if not text: return "", removed_chars
891
+ if not text:
892
+ return "", removed_chars
829
893
  processed_text = "".join(c for c in text if ord(c) >= 32)
830
894
  if allowed_chars != CHARS.ALL:
831
895
  filtered_text = ""
@@ -846,8 +910,8 @@ class Console:
846
910
  def insert_text_event(event: KeyPressEvent) -> None:
847
911
  nonlocal result_text, filtered_chars
848
912
  try:
849
- insert_text = event.data
850
- if not insert_text: return
913
+ if not (insert_text := event.data):
914
+ return
851
915
  buffer = event.app.current_buffer
852
916
  cursor_pos = buffer.cursor_position
853
917
  insert_text, filtered_chars = process_insert_text(insert_text)
@@ -928,7 +992,8 @@ class Console:
928
992
  FormatCodes.print(end, end="")
929
993
 
930
994
  if result_text in ("", None):
931
- if has_default: return default_val
995
+ if has_default:
996
+ return default_val
932
997
  result_text = ""
933
998
 
934
999
  if output_type == str:
@@ -937,10 +1002,30 @@ class Console:
937
1002
  try:
938
1003
  return output_type(result_text) # type: ignore[call-arg]
939
1004
  except (ValueError, TypeError):
940
- if has_default: return default_val
1005
+ if has_default:
1006
+ return default_val
941
1007
  raise
942
1008
 
943
1009
 
1010
+ class _ProgressUpdater(Protocol):
1011
+ """Protocol for progress update function with proper type hints."""
1012
+
1013
+ @overload
1014
+ def __call__(self, current: int) -> None:
1015
+ """Update the current progress value."""
1016
+ ...
1017
+
1018
+ @overload
1019
+ def __call__(self, current: int, label: str) -> None:
1020
+ """Update both current progress value and label."""
1021
+ ...
1022
+
1023
+ @overload
1024
+ def __call__(self, *, label: str) -> None:
1025
+ """Update the progress label only (keyword-only)."""
1026
+ ...
1027
+
1028
+
944
1029
  class ProgressBar:
945
1030
  """A console progress bar with smooth transitions and customizable appearance.\n
946
1031
  -------------------------------------------------------------------------------------------------
@@ -1067,23 +1152,69 @@ class ProgressBar:
1067
1152
  self._stop_intercepting()
1068
1153
 
1069
1154
  @contextmanager
1070
- def progress_context(self, total: int, label: Optional[str] = None) -> Generator[Callable[[int], None], None, None]:
1155
+ def progress_context(self, total: int, label: Optional[str] = None) -> Generator[_ProgressUpdater, None, None]:
1071
1156
  """Context manager for automatic cleanup. Returns a function to update progress.\n
1072
- --------------------------------------------------------------------------------------
1157
+ ----------------------------------------------------------------------------------------------------
1073
1158
  - `total` -⠀the total value representing 100% progress (must be greater than `0`)
1074
1159
  - `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder
1075
- --------------------------------------------------------------------------------------
1160
+ ----------------------------------------------------------------------------------------------------
1161
+ The returned callable accepts keyword arguments. At least one of these parameters must be provided:
1162
+ - `current` -⠀update the current progress value
1163
+ - `label` -⠀update the progress label\n
1164
+
1076
1165
  Example usage:
1077
1166
  ```python
1078
- with ProgressBar().progress_context(500, "Loading") as update_progress:
1079
- for i in range(500):
1167
+ with ProgressBar().progress_context(500, "Loading...") as update_progress:
1168
+ update_progress(0) # Show empty bar at start
1169
+
1170
+ for i in range(400):
1080
1171
  # Do some work...
1081
1172
  update_progress(i) # Update progress
1173
+
1174
+ update_progress(label="Finalizing...") # Update label
1175
+
1176
+ for i in range(400, 500):
1177
+ # Do some work...
1178
+ update_progress(i, f"Finalizing ({i})") # Update both
1082
1179
  ```"""
1180
+ current_progress = 0
1181
+ current_label = label
1182
+
1083
1183
  try:
1084
1184
 
1085
- def update_progress(current: int) -> None:
1086
- self.show_progress(current, total, label)
1185
+ def update_progress(*args, **kwargs) -> None: # TYPE HINTS DEFINED IN '_ProgressUpdater' PROTOCOL
1186
+ """Update the progress bar's current value and/or label."""
1187
+ nonlocal current_progress, current_label
1188
+ current = label = None
1189
+
1190
+ if len(args) > 2:
1191
+ raise TypeError(f"update_progress() takes at most 2 positional arguments ({len(args)} given)")
1192
+ elif len(args) >= 1:
1193
+ current = args[0]
1194
+ if len(args) >= 2:
1195
+ label = args[1]
1196
+
1197
+ if "current" in kwargs:
1198
+ if current is not None:
1199
+ raise TypeError("update_progress() got multiple values for argument 'current'")
1200
+ current = kwargs["current"]
1201
+ if "label" in kwargs:
1202
+ if label is not None:
1203
+ raise TypeError("update_progress() got multiple values for argument 'label'")
1204
+ label = kwargs["label"]
1205
+
1206
+ if unexpected := set(kwargs.keys()) - {"current", "label"}:
1207
+ raise TypeError(f"update_progress() got unexpected keyword argument(s): {', '.join(unexpected)}")
1208
+
1209
+ if current is None and label is None:
1210
+ raise TypeError("At least one of 'current' or 'label' must be provided")
1211
+
1212
+ if current is not None:
1213
+ current_progress = current
1214
+ if label is not None:
1215
+ current_label = label
1216
+
1217
+ self.show_progress(current_progress, total, current_label)
1087
1218
 
1088
1219
  yield update_progress
1089
1220
  except Exception:
xulbux/data.py CHANGED
@@ -24,11 +24,14 @@ class Data:
24
24
  except UnicodeDecodeError:
25
25
  pass
26
26
  return {key: _base64.b64encode(data).decode("utf-8"), "encoding": "base64"}
27
- raise TypeError("Unsupported data type")
27
+ raise TypeError(f"Unsupported data type '{type(data)}'")
28
28
 
29
29
  @staticmethod
30
30
  def deserialize_bytes(obj: dict[str, str]) -> bytes | bytearray:
31
- """Converts a JSON-compatible bytes/bytearray format (dictionary) back to its original type."""
31
+ """Tries to converts a JSON-compatible bytes/bytearray format (dictionary) back to its original type.\n
32
+ --------------------------------------------------------------------------------------------------------
33
+ If the serialized object was created with `Data.serialize_bytes()`, it will work.
34
+ If it fails to decode the data, it will raise a `ValueError`."""
32
35
  for key in ("bytes", "bytearray"):
33
36
  if key in obj and "encoding" in obj:
34
37
  if obj["encoding"] == "utf-8":
@@ -36,9 +39,9 @@ class Data:
36
39
  elif obj["encoding"] == "base64":
37
40
  data = _base64.b64decode(obj[key].encode("utf-8"))
38
41
  else:
39
- raise ValueError("Unknown encoding method")
42
+ raise ValueError(f"Unknown encoding method '{obj['encoding']}'")
40
43
  return bytearray(data) if key == "bytearray" else data
41
- raise ValueError("Invalid serialized data")
44
+ raise ValueError(f"Invalid serialized data: {obj}")
42
45
 
43
46
  @staticmethod
44
47
  def chars_count(data: DataStructure) -> int:
@@ -387,7 +390,7 @@ class Data:
387
390
 
388
391
  valid_entries = [(path_id, new_val) for path_id, new_val in update_values.items()]
389
392
  if not valid_entries:
390
- raise ValueError(f"No valid update_values found in dictionary: {update_values}")
393
+ raise ValueError(f"No valid 'update_values' found in dictionary: {update_values}")
391
394
  for path_id, new_val in valid_entries:
392
395
  path = Data.__sep_path_id(path_id)
393
396
  data = update_nested(data, path, new_val)
@@ -581,7 +584,7 @@ class Data:
581
584
  @staticmethod
582
585
  def __sep_path_id(path_id: str) -> list[int]:
583
586
  if path_id.count(">") != 1:
584
- raise ValueError(f"Invalid path ID: {path_id}")
587
+ raise ValueError(f"Invalid path ID '{path_id}'")
585
588
  id_part_len = int(path_id.split(">")[0])
586
589
  path_ids_str = path_id.split(">")[1]
587
590
  return [int(path_ids_str[i:i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
xulbux/format_codes.py CHANGED
@@ -166,6 +166,7 @@ from typing import Optional, cast
166
166
  import ctypes as _ctypes
167
167
  import regex as _rx
168
168
  import sys as _sys
169
+ import os as _os
169
170
  import re as _re
170
171
 
171
172
 
@@ -419,11 +420,16 @@ class FormatCodes:
419
420
  global _CONSOLE_ANSI_CONFIGURED
420
421
  if not _CONSOLE_ANSI_CONFIGURED:
421
422
  _sys.stdout.flush()
422
- kernel32 = _ctypes.windll.kernel32
423
- h = kernel32.GetStdHandle(-11)
424
- mode = _ctypes.c_ulong()
425
- kernel32.GetConsoleMode(h, _ctypes.byref(mode))
426
- kernel32.SetConsoleMode(h, mode.value | 0x0004)
423
+ if _os.name == "nt":
424
+ try:
425
+ # ENABLE VT100 MODE ON WINDOWS TO BE ABLE TO USE ANSI CODES
426
+ kernel32 = _ctypes.windll.kernel32
427
+ h = kernel32.GetStdHandle(-11)
428
+ mode = _ctypes.c_ulong()
429
+ kernel32.GetConsoleMode(h, _ctypes.byref(mode))
430
+ kernel32.SetConsoleMode(h, mode.value | 0x0004)
431
+ except Exception:
432
+ pass
427
433
  _CONSOLE_ANSI_CONFIGURED = True
428
434
 
429
435
  @staticmethod
xulbux/json.py CHANGED
@@ -134,7 +134,7 @@ class Json:
134
134
  current[idx] = [] if next_key.isdigit() else {}
135
135
  current = current[idx]
136
136
  else:
137
- raise TypeError(f"Cannot navigate through {type(current).__name__}")
137
+ raise TypeError(f"Cannot navigate through '{type(current).__name__}'")
138
138
  return data_obj
139
139
 
140
140
  update = {}
xulbux/path.py CHANGED
@@ -46,7 +46,7 @@ class Path:
46
46
  raise_error: bool = False,
47
47
  use_closest_match: bool = False,
48
48
  ) -> Optional[str]:
49
- """Tries to locate and extend a relative path to an absolute path.\n
49
+ """Tries to resolve and extend a relative path to an absolute path.\n
50
50
  --------------------------------------------------------------------------------
51
51
  If the `rel_path` couldn't be located in predefined directories, it will be
52
52
  searched in the `search_in` directory/s. If the `rel_path` is still not found,
xulbux/system.py CHANGED
@@ -1,3 +1,6 @@
1
+ from .format_codes import FormatCodes
2
+ from .console import Console
3
+
1
4
  from typing import Optional
2
5
  import subprocess as _subprocess
3
6
  import platform as _platform
@@ -67,11 +70,25 @@ class System:
67
70
  raise NotImplementedError(f"Restart not implemented for `{system}`")
68
71
 
69
72
  @staticmethod
70
- def check_libs(lib_names: list[str], install_missing: bool = False, confirm_install: bool = True) -> Optional[list[str]]:
71
- """Checks if the given list of libraries are installed. If not:
72
- - If `install_missing` is false, the missing libraries will be returned as a list.
73
- - If `install_missing` is true, the missing libraries will be installed.
74
- - If `confirm_install` is true, the user will first be asked if they want to install the missing libraries."""
73
+ def check_libs(
74
+ lib_names: list[str],
75
+ install_missing: bool = False,
76
+ missing_libs_msgs: tuple[str, str] = (
77
+ "The following required libraries are missing:",
78
+ "Do you want to install them now?",
79
+ ),
80
+ confirm_install: bool = True,
81
+ ) -> Optional[list[str]]:
82
+ """Checks if the given list of libraries are installed and optionally installs missing libraries.\n
83
+ ------------------------------------------------------------------------------------------------------------
84
+ - `lib_names` -⠀a list of library names to check
85
+ - `install_missing` -⠀whether to directly missing libraries will be installed automatically using pip
86
+ - `missing_libs_msgs` -⠀two messages: the first one is displayed when missing libraries are found,
87
+ the second one is the confirmation message before installing missing libraries
88
+ - `confirm_install` -⠀whether the user will be asked for confirmation before installing missing libraries\n
89
+ ------------------------------------------------------------------------------------------------------------
90
+ If some libraries are missing or they could not be installed, their names will be returned as a list.
91
+ If all libraries are installed (or were installed successfully), `None` will be returned."""
75
92
  missing = []
76
93
  for lib in lib_names:
77
94
  try:
@@ -83,14 +100,22 @@ class System:
83
100
  elif not install_missing:
84
101
  return missing
85
102
  if confirm_install:
86
- print("The following required libraries are missing:")
103
+ FormatCodes.print(f"[b]({missing_libs_msgs[0]})")
87
104
  for lib in missing:
88
- print(f"- {lib}")
89
- if input("Do you want to install them now (Y/n): ").strip().lower() not in ("", "y", "yes"):
90
- raise ImportError("Missing required libraries.")
105
+ FormatCodes.print(f" [dim](•) [i]{lib}[_i]")
106
+ print()
107
+ if not Console.confirm(missing_libs_msgs[1], end="\n"):
108
+ return missing
91
109
  try:
92
- _subprocess.check_call([_sys.executable, "-m", "pip", "install"] + missing)
93
- return None
110
+ for lib in missing:
111
+ try:
112
+ _subprocess.check_call([_sys.executable, "-m", "pip", "install", lib])
113
+ missing.remove(lib)
114
+ except _subprocess.CalledProcessError:
115
+ pass
116
+ if len(missing) == 0:
117
+ return None
118
+ return missing
94
119
  except _subprocess.CalledProcessError:
95
120
  return missing
96
121
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xulbux
3
- Version: 1.8.2
3
+ Version: 1.8.3
4
4
  Summary: A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler.
5
5
  Author-email: XulbuX <xulbux.real@gmail.com>
6
6
  Maintainer-email: XulbuX <xulbux.real@gmail.com>
@@ -0,0 +1,20 @@
1
+ xulbux/__init__.py,sha256=qGusxHHAnHPl-qVU9Rj1_WaOOlNCf_cRbyCUTF0wvBM,935
2
+ xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
3
+ xulbux/color.py,sha256=tTNH30Wf2QNmyopKz4LADdels_TDzDMCptCtm76KkNo,50375
4
+ xulbux/console.py,sha256=5Z7pHp6hNXSHpN5YVQnGWhw5USZR9AJ7xKjN98ssuLE,63288
5
+ xulbux/data.py,sha256=U9T3lLEzas3A5V8Z0jPKnTy8WPhBmmF2bit8J4VkeB8,31219
6
+ xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
7
+ xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
8
+ xulbux/format_codes.py,sha256=kT1vn8_aaOelzkhy2NYcCJPX_n_LHFRVaXbeDLA1Ir8,25020
9
+ xulbux/json.py,sha256=Lo8vraQ3c-BoKFNYbXCdcJndh8tSsP4CMoYoEKarrmc,7450
10
+ xulbux/path.py,sha256=_Xm4k5aOk2CGZ_ErMbnJPzonF03L8pq8nT8Wy44l8Qo,7674
11
+ xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
12
+ xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
13
+ xulbux/system.py,sha256=CAV-mpTGEJ-0VWVRnTqWqhIsqS1HrIlM3LZOcTz9YBU,7793
14
+ xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
15
+ xulbux/cli/help.py,sha256=iKSEm1zEmng-BpfEa9xYvnIsTnJbZ_sRvMk4NtavwGY,4403
16
+ xulbux-1.8.3.dist-info/METADATA,sha256=C67KKJKYblIESkTwM3Gyh9jlFtWQ4ToRUaY8wORdhAM,11042
17
+ xulbux-1.8.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ xulbux-1.8.3.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
19
+ xulbux-1.8.3.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
20
+ xulbux-1.8.3.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- xulbux/__init__.py,sha256=NnqeQz_8_t4bkghp5qN3vQEm_Dff6AJBghJsTUVUaks,935
2
- xulbux/code.py,sha256=CLA3wh6mTvcXTlQgBeFaaLIBciHvXWqVM56rAtfO-JE,6102
3
- xulbux/color.py,sha256=tTNH30Wf2QNmyopKz4LADdels_TDzDMCptCtm76KkNo,50375
4
- xulbux/console.py,sha256=b9B1lQzAl5OEhmIlCbTyCyu_WRZwMVOuI-v1GfGtxzo,57308
5
- xulbux/data.py,sha256=hB9JxrSC7_6kelJ_TwOaapNrlig-jiyNZS3YoiJX8F8,30884
6
- xulbux/env_path.py,sha256=HGOSffdIDubzczsXe6umyqotrzqhIW83QBkISAamXT8,4157
7
- xulbux/file.py,sha256=7pa0-WS_DpXq7HRB1fLS6Acd9CM-ozXPpNJvMqCW4fw,2624
8
- xulbux/format_codes.py,sha256=UwJOH3JJebylYd-MclDvKOG72fhuku63JzZU_eUzMtA,24764
9
- xulbux/json.py,sha256=Ei5FdCjfM0FrrAEBmuuTcexl7mUY4eirXr-QPct2OS0,7448
10
- xulbux/path.py,sha256=lLAEVZrW0TAwCewlONFVQcQ_8tVn9LTJZVOZpeGvE5s,7673
11
- xulbux/regex.py,sha256=_BtMHRDNcD9zF4SL87dQuUVZcYGfZx9H5YNSDiEtzm8,8059
12
- xulbux/string.py,sha256=QaTo0TQ9m_2USNgQNaVw5ivQt-A1E-e5x8OpIB3xIlY,5561
13
- xulbux/system.py,sha256=uCjQhqlfOoLkEBdjWn5cLCEnaI4Ac3o9cwRKev07eWI,6612
14
- xulbux/base/consts.py,sha256=HwgI_Cr_U2QznezN17SP1j-gpyf3tsbu_KHMd8aKzyw,5918
15
- xulbux/cli/help.py,sha256=wlDTjFhyWQucywMGDmrYIjYn1TV__TwMlYzeSCkQ__U,4403
16
- xulbux-1.8.2.dist-info/METADATA,sha256=h7DU1OI_NtZNXoq6pFPP1dQhYnDlcbdEbaSsgYajXdc,11042
17
- xulbux-1.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
- xulbux-1.8.2.dist-info/entry_points.txt,sha256=aYh89GfiBOB8vw2VPgC6rhBinhnJoAL1kig-3lq_zkg,58
19
- xulbux-1.8.2.dist-info/top_level.txt,sha256=FkK4EZajwfP36fnlrPaR98OrEvZpvdEOdW1T5zTj6og,7
20
- xulbux-1.8.2.dist-info/RECORD,,
File without changes