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