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.
- {xulbux-1.8.1 → xulbux-1.8.2}/PKG-INFO +2 -2
- {xulbux-1.8.1 → xulbux-1.8.2}/pyproject.toml +4 -2
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/__init__.py +17 -2
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/cli/help.py +30 -2
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/color.py +12 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/console.py +305 -6
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/format_codes.py +4 -3
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/system.py +1 -1
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/PKG-INFO +2 -2
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/SOURCES.txt +3 -1
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_console.py +389 -2
- xulbux-1.8.2/tests/test_regex.py +305 -0
- xulbux-1.8.2/tests/test_system.py +87 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/README.md +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/setup.cfg +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/base/consts.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/code.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/data.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/env_path.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/file.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/json.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/path.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/regex.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux/string.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/dependency_links.txt +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/entry_points.txt +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/requires.txt +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/src/xulbux.egg-info/top_level.txt +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_code.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_color.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_color_types.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_data.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_env_path.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_file.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_format_codes.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_json.py +0 -0
- {xulbux-1.8.1 → xulbux-1.8.2}/tests/test_path.py +0 -0
- {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.
|
|
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.
|
|
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
|
+
__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__ = [
|
|
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
|
-
+
|
|
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
|
-
|
|
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
|
-
|
|
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"(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|