Skip to content

Commit c59cce3

Browse files
committed
update 1.8.2
1 parent cd56a03 commit c59cce3

8 files changed

Lines changed: 885 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
* spaces between a format code and the auto-reset-brackets are no longer allowed, so `[red]␣(text)` will not be automatically reset and output as `␣(text)`
2323
* added a new class to `ProgressBar` to the `console` module
2424
* made small performance improvement in `FormatCodes.to_ansi()`
25+
* added missing docstrings to several public class variables
26+
* added the missing tests for methods in the `console` module
27+
* added test for the last two modules that didn't have test until now: `regex` and `system`
2528

2629
## 20.08.2025 `v1.8.1`
2730
* **fixed a critical bug which caused the package to not install properly and make the whole library not work**

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,7 @@ testpaths = [
163163
"tests/test_format_codes.py",
164164
"tests/test_json.py",
165165
"tests/test_path.py",
166+
"tests/test_regex.py",
166167
"tests/test_string.py",
168+
"tests/test_system.py",
167169
]

src/xulbux/color.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,13 @@ class rgba:
9292

9393
def __init__(self, r: int, g: int, b: int, a: Optional[float] = None, _validate: bool = True):
9494
self.r: int
95+
"""The red channel (`0`–`255`)"""
9596
self.g: int
97+
"""The green channel (`0`–`255`)"""
9698
self.b: int
99+
"""The blue channel (`0`–`255`)"""
97100
self.a: Optional[float]
101+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
98102
if not _validate:
99103
self.r, self.g, self.b, self.a = r, g, b, a
100104
return
@@ -291,9 +295,13 @@ class hsla:
291295

292296
def __init__(self, h: int, s: int, l: int, a: Optional[float] = None, _validate: bool = True):
293297
self.h: int
298+
"""The hue channel (`0`–`360`)"""
294299
self.s: int
300+
"""The saturation channel (`0`–`100`)"""
295301
self.l: int
302+
"""The lightness channel (`0`–`100`)"""
296303
self.a: Optional[float]
304+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
297305
if not _validate:
298306
self.h, self.s, self.l, self.a = h, s, l, a
299307
return
@@ -496,9 +504,13 @@ def __init__(
496504
_a: Optional[float] = None,
497505
):
498506
self.r: int
507+
"""The red channel (`0`–`255`)"""
499508
self.g: int
509+
"""The green channel (`0`–`255`)"""
500510
self.b: int
511+
"""The blue channel (`0`–`255`)"""
501512
self.a: Optional[float]
513+
"""The alpha channel (`0.0`–`1.0`) or `None` if not set"""
502514
if all(x is not None for x in (_r, _g, _b)):
503515
self.r, self.g, self.b, self.a = cast(int, _r), cast(int, _g), cast(int, _b), _a
504516
return

src/xulbux/console.py

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ class ArgResult:
7878

7979
def __init__(self, exists: bool, value: Any | list[Any]):
8080
self.exists: bool = exists
81+
"""Whether the argument was found or not."""
8182
self.value: Any = value
83+
"""The value given with the found argument."""
8284

8385
def __bool__(self):
8486
return self.exists
@@ -969,55 +971,39 @@ def __init__(
969971
):
970972
self.active: bool = False
971973
"""Whether the progress bar is currently active (intercepting stdout) or not."""
972-
self.min_width: int = max(1, min_width)
974+
self.min_width: int
973975
"""The min width of the progress bar in chars."""
974-
self.max_width: int = max(self.min_width, max_width)
976+
self.max_width: int
975977
"""The max width of the progress bar in chars."""
976-
self.bar_format: str = bar_format
978+
self.bar_format: str
977979
"""The format string used to render the progress bar."""
978-
self.limited_bar_format: str = limited_bar_format
980+
self.limited_bar_format: str
979981
"""The simplified format string used when the console width is too small."""
980-
self.chars: tuple[str, ...] = chars
982+
self.chars: tuple[str, ...]
981983
"""A tuple of characters ordered from full to empty progress."""
982984

985+
self.set_width(min_width, max_width)
986+
self.set_bar_format(bar_format, limited_bar_format)
987+
self.set_chars(chars)
988+
983989
self._buffer: list[str] = []
984990
self._original_stdout: Optional[TextIO] = None
985991
self._current_progress_str: str = ""
986992
self._last_line_len: int = 0
987993

988-
def show_progress(self, current: int, total: int, label: Optional[str] = None) -> None:
989-
"""Show or update the progress bar.\n
990-
-----------------------------------------------------------------------------------------
991-
- `current` -⠀the current progress value (below 0 or greater than `total` hides the bar)
992-
- `total` -⠀the total value representing 100% progress (must be greater than 0)
993-
- `label` -⠀an optional label to display alongside the progress bar"""
994-
if total <= 0:
995-
raise ValueError("Total must be greater than 0.")
996-
997-
try:
998-
if not self.active:
999-
self._start_intercepting()
1000-
self._flush_buffer()
1001-
self._draw_progress_bar(current, total, label or "")
1002-
if current < 0 or current > total:
1003-
self.hide_progress()
1004-
except Exception:
1005-
self._emergency_cleanup()
1006-
raise
1007-
1008-
def hide_progress(self) -> None:
1009-
"""Hide the progress bar and restore normal console output."""
1010-
if self.active:
1011-
self._clear_progress_line()
1012-
self._stop_intercepting()
1013-
1014994
def set_width(self, min_width: Optional[int] = None, max_width: Optional[int] = None) -> None:
1015995
"""Set the width of the progress bar.\n
1016996
--------------------------------------------------------------
1017997
- `min_width` -⠀the min width of the progress bar in chars
1018998
- `max_width` -⠀the max width of the progress bar in chars"""
1019-
self.min_width = self.min_width if min_width is None else max(1, min_width)
1020-
self.max_width = self.max_width if max_width is None else max(self.min_width, max_width)
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)
10211007

10221008
def set_bar_format(self, bar_format: Optional[str] = None, limited_bar_format: Optional[str] = None) -> None:
10231009
"""Set the format string used to render the progress bar.\n
@@ -1032,17 +1018,79 @@ def set_bar_format(self, bar_format: Optional[str] = None, limited_bar_format: O
10321018
--------------------------------------------------------------------------------------------------
10331019
The bar format (also limited) can additionally be formatted with special formatting codes. For
10341020
more detailed information about formatting codes, see the `format_codes` module documentation."""
1035-
self.bar_format = "{l} |{b}| [b]({c})/{t} [dim](([i]({p}%)))" if bar_format is None else bar_format
1036-
self.limited_bar_format = "|{b}|" if limited_bar_format is None else limited_bar_format
1037-
1038-
def set_chars(self, chars: Optional[tuple[str, ...]] = None) -> None:
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:
10391031
"""Set the characters used to render the progress bar.\n
10401032
--------------------------------------------------------------------------
10411033
- `chars` -⠀a tuple of characters ordered from full to empty progress<br>
10421034
The first character represents completely filled sections, intermediate
10431035
characters create smooth transitions, and the last character represents
10441036
empty sections. If None, uses default Unicode block characters."""
1045-
self.chars = ("█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", " ") if chars is None else chars
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()
10461094

10471095
def _start_intercepting(self) -> None:
10481096
self.active = True
@@ -1132,32 +1180,6 @@ def _redraw_progress_bar(self) -> None:
11321180
self._original_stdout.write(f"{self._current_progress_str}")
11331181
self._original_stdout.flush()
11341182

1135-
@contextmanager
1136-
def progress_context(self, total: int, label: Optional[str] = None) -> Generator[Callable[[int], None], None, None]:
1137-
"""Context manager for automatic cleanup. Returns a function to update progress.\n
1138-
-----------------------------------------------------------------------------------
1139-
- `total` -⠀the total value representing 100% progress
1140-
- `label` -⠀an optional label to display alongside the progress bar
1141-
-----------------------------------------------------------------------------------
1142-
Example usage:
1143-
```python
1144-
with ProgressBar().progress_context(500, "Loading") as update_progress:
1145-
for i in range(501):
1146-
# Do some work...
1147-
update_progress(i) # Update progress
1148-
```"""
1149-
try:
1150-
1151-
def update_progress(current: int) -> None:
1152-
self.show_progress(current, total, label)
1153-
1154-
yield update_progress
1155-
except Exception:
1156-
self._emergency_cleanup()
1157-
raise
1158-
finally:
1159-
self.hide_progress()
1160-
11611183

11621184
class _InterceptedOutput(_io.StringIO):
11631185
"""Custom StringIO that captures output and stores it in the progress bar buffer."""

src/xulbux/system.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class _IsElevated:
1212
def __get__(self, obj, owner=None):
1313
try:
1414
if _os.name == "nt":
15-
return _ctypes.windll.shell32.IsUserAnAdmin() != 0
15+
return _ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
1616
elif _os.name == "posix":
1717
return _os.geteuid() == 0 # type: ignore[attr-defined]
1818
except Exception:

0 commit comments

Comments
 (0)