xulbux 1.8.1__tar.gz → 1.8.2__tar.gz

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.

Files changed (38) hide show
  1. {xulbux-1.8.1 → xulbux-1.8.2}/PKG-INFO +2 -2
  2. {xulbux-1.8.1 → xulbux-1.8.2}/pyproject.toml +4 -2
  3. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/__init__.py +17 -2
  4. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/cli/help.py +30 -2
  5. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/color.py +12 -0
  6. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/console.py +305 -6
  7. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/format_codes.py +4 -3
  8. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/system.py +1 -1
  9. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/PKG-INFO +2 -2
  10. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/SOURCES.txt +3 -1
  11. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_console.py +389 -2
  12. xulbux-1.8.2/tests/test_regex.py +305 -0
  13. xulbux-1.8.2/tests/test_system.py +87 -0
  14. {xulbux-1.8.1 → xulbux-1.8.2}/README.md +0 -0
  15. {xulbux-1.8.1 → xulbux-1.8.2}/setup.cfg +0 -0
  16. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/base/consts.py +0 -0
  17. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/code.py +0 -0
  18. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/data.py +0 -0
  19. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/env_path.py +0 -0
  20. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/file.py +0 -0
  21. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/json.py +0 -0
  22. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/path.py +0 -0
  23. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/regex.py +0 -0
  24. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/string.py +0 -0
  25. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/dependency_links.txt +0 -0
  26. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/entry_points.txt +0 -0
  27. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/requires.txt +0 -0
  28. {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/top_level.txt +0 -0
  29. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_code.py +0 -0
  30. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_color.py +0 -0
  31. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_color_types.py +0 -0
  32. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_data.py +0 -0
  33. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_env_path.py +0 -0
  34. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_file.py +0 -0
  35. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_format_codes.py +0 -0
  36. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_json.py +0 -0
  37. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_path.py +0 -0
  38. {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_string.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xulbux
3
- Version: 1.8.1
4
- Summary: A Python library which includes lots of helpful classes, types and functions aiming to make common programming tasks simpler.
3
+ Version: 1.8.2
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>
7
7
  License-Expression: MIT
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xulbux"
7
- version = "1.8.1"
7
+ version = "1.8.2"
8
8
  authors = [{ name = "XulbuX", email = "xulbux.real@gmail.com" }]
9
9
  maintainers = [{ name = "XulbuX", email = "xulbux.real@gmail.com" }]
10
- description = "A Python library which includes lots of helpful classes, types and functions aiming to make common programming tasks simpler."
10
+ description = "A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler."
11
11
  readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ["LICEN[CS]E.*"]
@@ -163,5 +163,7 @@ testpaths = [
163
163
  "tests/test_format_codes.py",
164
164
  "tests/test_json.py",
165
165
  "tests/test_path.py",
166
+ "tests/test_regex.py",
166
167
  "tests/test_string.py",
168
+ "tests/test_system.py",
167
169
  ]
@@ -1,4 +1,4 @@
1
- __version__ = "1.8.1"
1
+ __version__ = "1.8.2"
2
2
 
3
3
  __author__ = "XulbuX"
4
4
  __email__ = "xulbux.real@gmail.com"
@@ -7,11 +7,26 @@ __copyright__ = "Copyright (c) 2024 XulbuX"
7
7
  __url__ = "https://github.com/XulbuX/PythonLibraryXulbuX"
8
8
  __description__ = "A Python library which includes lots of helpful classes, types, and functions aiming to make common programming tasks simpler."
9
9
 
10
- __all__ = ["Code", "Color", "Console", "Data", "EnvPath", "File", "FormatCodes", "Json", "Path", "Regex", "String", "System"]
10
+ __all__ = [
11
+ "Code",
12
+ "Color",
13
+ "Console",
14
+ "Data",
15
+ "EnvPath",
16
+ "File",
17
+ "FormatCodes",
18
+ "Json",
19
+ "Path",
20
+ "ProgressBar",
21
+ "Regex",
22
+ "String",
23
+ "System",
24
+ ]
11
25
 
12
26
  from .code import Code
13
27
  from .color import Color
14
28
  from .console import Console
29
+ from .console import ProgressBar
15
30
  from .data import Data
16
31
  from .env_path import EnvPath
17
32
  from .file import File
@@ -3,22 +3,50 @@ from ..base.consts import COLOR
3
3
  from ..format_codes import FormatCodes
4
4
  from ..console import Console
5
5
 
6
+ from urllib.error import HTTPError
7
+ from typing import Optional
8
+ import urllib.request as _request
9
+ import json as _json
6
10
 
11
+
12
+ def get_latest_version() -> Optional[str]:
13
+ with _request.urlopen(URL) as response:
14
+ if response.status == 200:
15
+ data = _json.load(response)
16
+ return data["info"]["version"]
17
+ else:
18
+ raise HTTPError(URL, response.status, "Failed to fetch latest version info", response.headers, None)
19
+
20
+
21
+ def is_latest_version() -> Optional[bool]:
22
+ try:
23
+ latest = get_latest_version()
24
+ if latest in ("", None): return None
25
+ latest_v_parts = tuple(int(part) for part in latest.lower().lstrip("v").split('.'))
26
+ installed_v_parts = tuple(int(part) for part in __version__.lower().lstrip("v").split('.'))
27
+ return latest_v_parts <= installed_v_parts
28
+ except Exception:
29
+ return None
30
+
31
+
32
+ URL = "https://pypi.org/pypi/xulbux/json"
33
+ IS_LATEST_VERSION = is_latest_version()
7
34
  CLR = {
8
35
  "class": COLOR.TANGERINE,
36
+ "code_border": COLOR.GRAY,
9
37
  "const": COLOR.RED,
10
38
  "func": COLOR.CYAN,
11
39
  "import": COLOR.NEON_GREEN,
12
40
  "lib": COLOR.ORANGE,
41
+ "notice": COLOR.YELLOW,
13
42
  "punctuators": COLOR.DARK_GRAY,
14
- "code_border": COLOR.GRAY,
15
43
  }
16
44
  HELP = FormatCodes.to_ansi(
17
45
  rf""" [_|b|#7075FF] __ __
18
46
  [b|#7075FF] _ __ __ __/ / / /_ __ ___ __
19
47
  [b|#7075FF] | |/ // / / / / / __ \/ / / | |/ /
20
48
  [b|#7075FF] > , </ /_/ / /_/ /_/ / /_/ /> , <
21
- [b|#7075FF]/_/|_|\____/\__/\____/\____//_/|_| [*|BG:{COLOR.GRAY}|#000] v[b]{__version__} [*]
49
+ [b|#7075FF]/_/|_|\____/\__/\____/\____//_/|_| [*|BG:{COLOR.GRAY}|#000] v[b]{__version__} [*|dim|{CLR['notice']}]({'' if IS_LATEST_VERSION else ' (newer available)'})[*]
22
50
 
23
51
  [i|{COLOR.CORAL}]A TON OF COOL FUNCTIONS, YOU NEED![*]
24
52
 
@@ -92,9 +92,13 @@ class rgba:
92
92
 
93
93
  def __init__(self, r: int, g: int, b: int, a: Optional[float] = None, _validate: bool = True):
94
94
  self.r: int
95
+ """The red channel (`0`–`255`)"""
95
96
  self.g: int
97
+ """The green channel (`0`–`255`)"""
96
98
  self.b: int
99
+ """The blue channel (`0`–`255`)"""
97
100
  self.a: Optional[float]
101
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
98
102
  if not _validate:
99
103
  self.r, self.g, self.b, self.a = r, g, b, a
100
104
  return
@@ -291,9 +295,13 @@ class hsla:
291
295
 
292
296
  def __init__(self, h: int, s: int, l: int, a: Optional[float] = None, _validate: bool = True):
293
297
  self.h: int
298
+ """The hue channel (`0`–`360`)"""
294
299
  self.s: int
300
+ """The saturation channel (`0`–`100`)"""
295
301
  self.l: int
302
+ """The lightness channel (`0`–`100`)"""
296
303
  self.a: Optional[float]
304
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
297
305
  if not _validate:
298
306
  self.h, self.s, self.l, self.a = h, s, l, a
299
307
  return
@@ -496,9 +504,13 @@ class hexa:
496
504
  _a: Optional[float] = None,
497
505
  ):
498
506
  self.r: int
507
+ """The red channel (`0`–`255`)"""
499
508
  self.g: int
509
+ """The green channel (`0`–`255`)"""
500
510
  self.b: int
511
+ """The blue channel (`0`–`255`)"""
501
512
  self.a: Optional[float]
513
+ """The alpha channel (`0.0`–`1.0`) or `None` if not set"""
502
514
  if all(x is not None for x in (_r, _g, _b)):
503
515
  self.r, self.g, self.b, self.a = cast(int, _r), cast(int, _g), cast(int, _b), _a
504
516
  return
@@ -5,15 +5,16 @@ You can also use special formatting codes directly inside the log message to cha
5
5
  For more detailed information about formatting codes, see the the `format_codes` module documentation.
6
6
  """
7
7
 
8
- from .base.consts import COLOR, CHARS
9
- from .format_codes import FormatCodes, _COMPILED
8
+ from .base.consts import COLOR, CHARS, ANSI
9
+ 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 Callable, Optional, Literal, Mapping, Any, cast
13
+ from typing import Generator, Callable, Optional, Literal, Mapping, Pattern, TypeVar, TextIO, Any, cast
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
17
+ from contextlib import contextmanager
17
18
  from prompt_toolkit.keys import Keys
18
19
  import prompt_toolkit as _pt
19
20
  import keyboard as _keyboard
@@ -21,6 +22,17 @@ import getpass as _getpass
21
22
  import shutil as _shutil
22
23
  import sys as _sys
23
24
  import os as _os
25
+ import re as _re
26
+ import io as _io
27
+
28
+
29
+ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
30
+ "label": _re.compile(r"(?i)\{(?:label|l)\}"),
31
+ "bar": _re.compile(r"(?i)\{(?:bar|b)\}"),
32
+ "current": _re.compile(r"(?i)\{(?:current|c)\}"),
33
+ "total": _re.compile(r"(?i)\{(?:total|t)\}"),
34
+ "percentage": _re.compile(r"(?i)\{(?:percentage|percent|p)\}"),
35
+ }
24
36
 
25
37
 
26
38
  class _ConsoleWidth:
@@ -66,7 +78,9 @@ class ArgResult:
66
78
 
67
79
  def __init__(self, exists: bool, value: Any | list[Any]):
68
80
  self.exists: bool = exists
81
+ """Whether the argument was found or not."""
69
82
  self.value: Any = value
83
+ """The value given with the found argument."""
70
84
 
71
85
  def __bool__(self):
72
86
  return self.exists
@@ -574,7 +588,7 @@ class Console:
574
588
  spaces_l = " " * indent
575
589
  lines = [
576
590
  f"{spaces_l}[bg:{box_bg_color}]{' ' * w_padding}"
577
- + _COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", line) +
591
+ + _FC_COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", line) +
578
592
  (" " * ((w_padding + max_line_len - len(unfmt)) + pad_w_full)) + "[*]" for line, unfmt in zip(lines, unfmt_lines)
579
593
  ]
580
594
  pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
@@ -729,6 +743,8 @@ class Console:
729
743
  FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
730
744
  return input_string
731
745
 
746
+ T = TypeVar("T")
747
+
732
748
  @staticmethod
733
749
  def input(
734
750
  prompt: object = "",
@@ -742,7 +758,9 @@ class Console:
742
758
  allowed_chars: str = CHARS.ALL, #type: ignore[assignment]
743
759
  allow_paste: bool = True,
744
760
  validator: Optional[Callable[[str], Optional[str]]] = None,
745
- ) -> str:
761
+ default_val: Optional[T] = None,
762
+ output_type: type[T] = str, # type: ignore[assignment]
763
+ ) -> T:
746
764
  """Acts like a standard Python `input()` a bunch of cool extra features.\n
747
765
  ------------------------------------------------------------------------------------
748
766
  - `prompt` -⠀the input prompt
@@ -758,12 +776,15 @@ class Console:
758
776
  - `allow_paste` -⠀whether to allow pasting text into the input or not
759
777
  - `validator` -⠀a function that takes the input string and returns a string error
760
778
  message if invalid, or nothing if valid
779
+ - `default_val` -⠀the default value to return if the input is empty
780
+ - `output_type` -⠀the type (class) to convert the input to before returning it\n
761
781
  ------------------------------------------------------------------------------------
762
782
  The input prompt can be formatted with special formatting codes. For more detailed
763
783
  information about formatting codes, see the `format_codes` module documentation."""
764
784
  result_text = ""
765
785
  tried_pasting = False
766
786
  filtered_chars = set()
787
+ has_default = default_val is not None
767
788
 
768
789
  class InputValidator(Validator):
769
790
 
@@ -905,4 +926,282 @@ class Console:
905
926
  FormatCodes.print(start, end="")
906
927
  session.prompt()
907
928
  FormatCodes.print(end, end="")
908
- return result_text
929
+
930
+ if result_text in ("", None):
931
+ if has_default: return default_val
932
+ result_text = ""
933
+
934
+ if output_type == str:
935
+ return result_text # type: ignore[return-value]
936
+ else:
937
+ try:
938
+ return output_type(result_text) # type: ignore[call-arg]
939
+ except (ValueError, TypeError):
940
+ if has_default: return default_val
941
+ raise
942
+
943
+
944
+ class ProgressBar:
945
+ """A console progress bar with smooth transitions and customizable appearance.\n
946
+ -------------------------------------------------------------------------------------------------
947
+ - `min_width` -⠀the min width of the progress bar in chars
948
+ - `max_width` -⠀the max width of the progress bar in chars
949
+ - `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
950
+ * `{label}` `{l}`
951
+ * `{bar}` `{b}`
952
+ * `{current}` `{c}`
953
+ * `{total}` `{t}`
954
+ * `{percentage}` `{percent}` `{p}`
955
+ - `limited_bar_format` -⠀a simplified format string used when the console width is too small
956
+ - `chars` -⠀a tuple of characters ordered from full to empty progress<br>
957
+ The first character represents completely filled sections, intermediate
958
+ characters create smooth transitions, and the last character represents
959
+ empty sections. Default is a set of Unicode block characters.
960
+ --------------------------------------------------------------------------------------------------
961
+ The bar format (also limited) can additionally be formatted with special formatting codes. For
962
+ more detailed information about formatting codes, see the `format_codes` module documentation."""
963
+
964
+ def __init__(
965
+ self,
966
+ min_width: int = 10,
967
+ max_width: int = 50,
968
+ bar_format: str = "{l} |{b}| [b]({c})/{t} [dim](([i]({p}%)))",
969
+ limited_bar_format: str = "|{b}|",
970
+ chars: tuple[str, ...] = ("█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", " "),
971
+ ):
972
+ self.active: bool = False
973
+ """Whether the progress bar is currently active (intercepting stdout) or not."""
974
+ self.min_width: int
975
+ """The min width of the progress bar in chars."""
976
+ self.max_width: int
977
+ """The max width of the progress bar in chars."""
978
+ self.bar_format: str
979
+ """The format string used to render the progress bar."""
980
+ self.limited_bar_format: str
981
+ """The simplified format string used when the console width is too small."""
982
+ self.chars: tuple[str, ...]
983
+ """A tuple of characters ordered from full to empty progress."""
984
+
985
+ self.set_width(min_width, max_width)
986
+ self.set_bar_format(bar_format, limited_bar_format)
987
+ self.set_chars(chars)
988
+
989
+ self._buffer: list[str] = []
990
+ self._original_stdout: Optional[TextIO] = None
991
+ self._current_progress_str: str = ""
992
+ self._last_line_len: int = 0
993
+
994
+ def set_width(self, min_width: Optional[int] = None, max_width: Optional[int] = None) -> None:
995
+ """Set the width of the progress bar.\n
996
+ --------------------------------------------------------------
997
+ - `min_width` -⠀the min width of the progress bar in chars
998
+ - `max_width` -⠀the max width of the progress bar in chars"""
999
+ if min_width is not None:
1000
+ if min_width < 1:
1001
+ raise ValueError("Minimum width must be at least 1.")
1002
+ self.min_width = max(1, min_width)
1003
+ if max_width is not None:
1004
+ if max_width < 1:
1005
+ raise ValueError("Maximum width must be at least 1.")
1006
+ self.max_width = max(self.min_width, max_width)
1007
+
1008
+ def set_bar_format(self, bar_format: Optional[str] = None, limited_bar_format: Optional[str] = None) -> None:
1009
+ """Set the format string used to render the progress bar.\n
1010
+ --------------------------------------------------------------------------------------------------
1011
+ - `bar_format` -⠀the format string used to render the progress bar, containing placeholders:
1012
+ * `{label}` `{l}`
1013
+ * `{bar}` `{b}`
1014
+ * `{current}` `{c}`
1015
+ * `{total}` `{t}`
1016
+ * `{percentage}` `{percent}` `{p}`
1017
+ - `limited_bar_format` -⠀a simplified format string used when the console width is too small
1018
+ --------------------------------------------------------------------------------------------------
1019
+ The bar format (also limited) can additionally be formatted with special formatting codes. For
1020
+ more detailed information about formatting codes, see the `format_codes` module documentation."""
1021
+ if bar_format is not None:
1022
+ if not _COMPILED["bar"].search(bar_format):
1023
+ raise ValueError("'bar_format' must contain the '{bar}' or '{b}' placeholder.")
1024
+ self.bar_format = bar_format
1025
+ if limited_bar_format is not None:
1026
+ if not _COMPILED["bar"].search(limited_bar_format):
1027
+ raise ValueError("'limited_bar_format' must contain the '{bar}' or '{b}' placeholder.")
1028
+ self.limited_bar_format = limited_bar_format
1029
+
1030
+ def set_chars(self, chars: tuple[str, ...]) -> None:
1031
+ """Set the characters used to render the progress bar.\n
1032
+ --------------------------------------------------------------------------
1033
+ - `chars` -⠀a tuple of characters ordered from full to empty progress<br>
1034
+ The first character represents completely filled sections, intermediate
1035
+ characters create smooth transitions, and the last character represents
1036
+ empty sections. If None, uses default Unicode block characters."""
1037
+ if len(chars) < 2:
1038
+ raise ValueError("'chars' must contain at least two characters (full and empty).")
1039
+ if not all(len(c) == 1 for c in chars if isinstance(c, str)):
1040
+ raise ValueError("All 'chars' items must be single-character strings.")
1041
+ self.chars = chars
1042
+
1043
+ def show_progress(self, current: int, total: int, label: Optional[str] = None) -> None:
1044
+ """Show or update the progress bar.\n
1045
+ -------------------------------------------------------------------------------------------
1046
+ - `current` -⠀the current progress value (below `0` or greater than `total` hides the bar)
1047
+ - `total` -⠀the total value representing 100% progress (must be greater than `0`)
1048
+ - `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder"""
1049
+ if total <= 0:
1050
+ raise ValueError("Total must be greater than 0.")
1051
+
1052
+ try:
1053
+ if not self.active:
1054
+ self._start_intercepting()
1055
+ self._flush_buffer()
1056
+ self._draw_progress_bar(current, total, label or "")
1057
+ if current < 0 or current > total:
1058
+ self.hide_progress()
1059
+ except Exception:
1060
+ self._emergency_cleanup()
1061
+ raise
1062
+
1063
+ def hide_progress(self) -> None:
1064
+ """Hide the progress bar and restore normal console output."""
1065
+ if self.active:
1066
+ self._clear_progress_line()
1067
+ self._stop_intercepting()
1068
+
1069
+ @contextmanager
1070
+ def progress_context(self, total: int, label: Optional[str] = None) -> Generator[Callable[[int], None], None, None]:
1071
+ """Context manager for automatic cleanup. Returns a function to update progress.\n
1072
+ --------------------------------------------------------------------------------------
1073
+ - `total` -⠀the total value representing 100% progress (must be greater than `0`)
1074
+ - `label` -⠀an optional label which is inserted at the `{label}` or `{l}` placeholder
1075
+ --------------------------------------------------------------------------------------
1076
+ Example usage:
1077
+ ```python
1078
+ with ProgressBar().progress_context(500, "Loading") as update_progress:
1079
+ for i in range(500):
1080
+ # Do some work...
1081
+ update_progress(i) # Update progress
1082
+ ```"""
1083
+ try:
1084
+
1085
+ def update_progress(current: int) -> None:
1086
+ self.show_progress(current, total, label)
1087
+
1088
+ yield update_progress
1089
+ except Exception:
1090
+ self._emergency_cleanup()
1091
+ raise
1092
+ finally:
1093
+ self.hide_progress()
1094
+
1095
+ def _start_intercepting(self) -> None:
1096
+ self.active = True
1097
+ self._original_stdout = _sys.stdout
1098
+ _sys.stdout = _InterceptedOutput(self)
1099
+
1100
+ def _stop_intercepting(self) -> None:
1101
+ if self._original_stdout:
1102
+ _sys.stdout = self._original_stdout
1103
+ self._original_stdout = None
1104
+ self.active = False
1105
+ self._buffer.clear()
1106
+ self._last_line_len = 0
1107
+ self._current_progress_str = ""
1108
+
1109
+ def _emergency_cleanup(self) -> None:
1110
+ """Emergency cleanup to restore stdout in case of exceptions."""
1111
+ try:
1112
+ self._stop_intercepting()
1113
+ except Exception:
1114
+ pass
1115
+
1116
+ def _flush_buffer(self) -> None:
1117
+ if self._buffer and self._original_stdout:
1118
+ self._clear_progress_line()
1119
+ for content in self._buffer:
1120
+ self._original_stdout.write(content)
1121
+ self._original_stdout.flush()
1122
+ self._buffer.clear()
1123
+
1124
+ def _draw_progress_bar(self, current: int, total: int, label: Optional[str] = None) -> None:
1125
+ if total <= 0 or not self._original_stdout:
1126
+ return
1127
+ percentage = min(100, (current / total) * 100)
1128
+ formatted, bar_width = self._get_formatted_info_and_bar_width(self.bar_format, current, total, percentage, label)
1129
+ if bar_width < self.min_width:
1130
+ formatted, bar_width = self._get_formatted_info_and_bar_width(
1131
+ self.limited_bar_format, current, total, percentage, label
1132
+ )
1133
+ bar = self._create_bar(current, total, max(1, bar_width)) + "[*]"
1134
+ progress_text = _COMPILED["bar"].sub(FormatCodes.to_ansi(bar), formatted)
1135
+ self._current_progress_str = progress_text
1136
+ self._last_line_len = len(progress_text)
1137
+ self._original_stdout.write(f"\r{progress_text}")
1138
+ self._original_stdout.flush()
1139
+
1140
+ def _get_formatted_info_and_bar_width(
1141
+ self,
1142
+ bar_format: str,
1143
+ current: int,
1144
+ total: int,
1145
+ percentage: float,
1146
+ label: Optional[str] = None,
1147
+ ) -> tuple[str, int]:
1148
+ formatted = _COMPILED["label"].sub(label or "", bar_format)
1149
+ formatted = _COMPILED["current"].sub(str(current), formatted)
1150
+ formatted = _COMPILED["total"].sub(str(total), formatted)
1151
+ formatted = _COMPILED["percentage"].sub(f"{percentage:.1f}", formatted)
1152
+ formatted = FormatCodes.to_ansi(formatted)
1153
+ bar_space = Console.w - len(FormatCodes.remove_ansi(_COMPILED["bar"].sub("", formatted)))
1154
+ bar_width = min(bar_space, self.max_width) if bar_space > 0 else 0
1155
+ return formatted, bar_width
1156
+
1157
+ def _create_bar(self, current: int, total: int, bar_width: int) -> str:
1158
+ progress = current / total if total > 0 else 0
1159
+ bar = []
1160
+
1161
+ for i in range(bar_width):
1162
+ pos_progress = (i + 1) / bar_width
1163
+ if progress >= pos_progress:
1164
+ bar.append(self.chars[0])
1165
+ elif progress >= pos_progress - (1 / bar_width):
1166
+ remainder = (progress - (pos_progress - (1 / bar_width))) * bar_width
1167
+ char_idx = len(self.chars) - 1 - min(int(remainder * len(self.chars)), len(self.chars) - 1)
1168
+ bar.append(self.chars[char_idx])
1169
+ else:
1170
+ bar.append(self.chars[-1])
1171
+ return "".join(bar)
1172
+
1173
+ def _clear_progress_line(self) -> None:
1174
+ if self._last_line_len > 0 and self._original_stdout:
1175
+ self._original_stdout.write(f"{ANSI.CHAR}[2K\r")
1176
+ self._original_stdout.flush()
1177
+
1178
+ def _redraw_progress_bar(self) -> None:
1179
+ if self._current_progress_str and self._original_stdout:
1180
+ self._original_stdout.write(f"{self._current_progress_str}")
1181
+ self._original_stdout.flush()
1182
+
1183
+
1184
+ class _InterceptedOutput(_io.StringIO):
1185
+ """Custom StringIO that captures output and stores it in the progress bar buffer."""
1186
+
1187
+ def __init__(self, progress_bar: ProgressBar):
1188
+ super().__init__()
1189
+ self.progress_bar = progress_bar
1190
+
1191
+ def write(self, content: str) -> int:
1192
+ try:
1193
+ if content and content != "\r":
1194
+ self.progress_bar._buffer.append(content)
1195
+ return len(content)
1196
+ except Exception:
1197
+ self.progress_bar._emergency_cleanup()
1198
+ raise
1199
+
1200
+ def flush(self) -> None:
1201
+ try:
1202
+ if self.progress_bar.active and self.progress_bar._buffer:
1203
+ self.progress_bar._flush_buffer()
1204
+ self.progress_bar._redraw_progress_bar()
1205
+ except Exception:
1206
+ self.progress_bar._emergency_cleanup()
1207
+ raise
@@ -190,7 +190,7 @@ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
190
190
  "ansi_seq": _re.compile(ANSI.CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
191
191
  "formatting": _rx.compile(
192
192
  Regex.brackets("[", "]", is_group=True, ignore_in_strings=False)
193
- + r"(?:\s*([/\\]?)\s*"
193
+ + r"(?:([/\\]?)"
194
194
  + Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False)
195
195
  + r")?"
196
196
  ),
@@ -317,6 +317,7 @@ class FormatCodes:
317
317
  ]
318
318
  if auto_reset_txt and not auto_reset_escaped:
319
319
  reset_keys = []
320
+ default_color_resets = ("_bg", "default") if use_default else ("_bg", "_c")
320
321
  for k in format_keys:
321
322
  k_lower = k.lower()
322
323
  k_set = set(k_lower.split(":"))
@@ -324,7 +325,7 @@ class FormatCodes:
324
325
  if k_set & _PREFIX["BR"]:
325
326
  for i in range(len(k)):
326
327
  if is_valid_color(k[i:]):
327
- reset_keys.extend(["_bg", "default"] if use_default else ["_bg", "_c"])
328
+ reset_keys.extend(default_color_resets)
328
329
  break
329
330
  else:
330
331
  for i in range(len(k)):
@@ -334,7 +335,7 @@ class FormatCodes:
334
335
  elif is_valid_color(k) or any(
335
336
  k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon):])
336
337
  for prefix in _PREFIX["BR"]):
337
- reset_keys.append("default" if use_default else "_c")
338
+ reset_keys.append(default_color_resets[1])
338
339
  else:
339
340
  reset_keys.append(f"_{k}")
340
341
  ansi_resets = [
@@ -12,7 +12,7 @@ class _IsElevated:
12
12
  def __get__(self, obj, owner=None):
13
13
  try:
14
14
  if _os.name == "nt":
15
- return _ctypes.windll.shell32.IsUserAnAdmin() != 0
15
+ return _ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
16
16
  elif _os.name == "posix":
17
17
  return _os.geteuid() == 0 # type: ignore[attr-defined]
18
18
  except Exception:
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xulbux
3
- Version: 1.8.1
4
- Summary: A Python library which includes lots of helpful classes, types and functions aiming to make common programming tasks simpler.
3
+ Version: 1.8.2
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>
7
7
  License-Expression: MIT
@@ -31,4 +31,6 @@ tests/test_file.py
31
31
  tests/test_format_codes.py
32
32
  tests/test_json.py
33
33
  tests/test_path.py
34
- tests/test_string.py
34
+ tests/test_regex.py
35
+ tests/test_string.py
36
+ tests/test_system.py