xulbux 1.6.9__py3-none-any.whl → 1.7.0__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 +146 -115
- xulbux/xx_console.py +55 -40
- xulbux/xx_data.py +22 -21
- xulbux/xx_env_path.py +6 -5
- xulbux/xx_file.py +2 -2
- xulbux/xx_format_codes.py +30 -25
- xulbux/xx_json.py +7 -6
- xulbux/xx_path.py +7 -7
- xulbux/xx_regex.py +6 -1
- xulbux/xx_system.py +5 -5
- {xulbux-1.6.9.dist-info → xulbux-1.7.0.dist-info}/METADATA +1 -1
- xulbux-1.7.0.dist-info/RECORD +21 -0
- {xulbux-1.6.9.dist-info → xulbux-1.7.0.dist-info}/WHEEL +1 -1
- xulbux-1.6.9.dist-info/RECORD +0 -21
- {xulbux-1.6.9.dist-info → xulbux-1.7.0.dist-info}/entry_points.txt +0 -0
- {xulbux-1.6.9.dist-info → xulbux-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {xulbux-1.6.9.dist-info → xulbux-1.7.0.dist-info}/top_level.txt +0 -0
xulbux/xx_console.py
CHANGED
|
@@ -8,7 +8,7 @@ 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
14
|
from typing import Optional, Any
|
|
@@ -103,14 +103,14 @@ class Args:
|
|
|
103
103
|
|
|
104
104
|
class Console:
|
|
105
105
|
|
|
106
|
-
w: int = _ConsoleWidth()
|
|
106
|
+
w: int = _ConsoleWidth() # type: ignore[assignment]
|
|
107
107
|
"""The width of the console in characters."""
|
|
108
|
-
h: int = _ConsoleHeight()
|
|
108
|
+
h: int = _ConsoleHeight() # type: ignore[assignment]
|
|
109
109
|
"""The height of the console in lines."""
|
|
110
|
-
wh: tuple[int, int] = _ConsoleSize()
|
|
110
|
+
wh: tuple[int, int] = _ConsoleSize() # type: ignore[assignment]
|
|
111
111
|
"""A tuple with the width and height of
|
|
112
112
|
the console in characters and lines."""
|
|
113
|
-
usr: str = _ConsoleUser()
|
|
113
|
+
usr: str = _ConsoleUser() # type: ignore[assignment]
|
|
114
114
|
"""The name of the current user."""
|
|
115
115
|
|
|
116
116
|
@staticmethod
|
|
@@ -249,8 +249,8 @@ class Console:
|
|
|
249
249
|
format_linebreaks: bool = True,
|
|
250
250
|
start: str = "",
|
|
251
251
|
end: str = "\n",
|
|
252
|
-
title_bg_color:
|
|
253
|
-
default_color:
|
|
252
|
+
title_bg_color: Optional[Rgba | Hexa] = None,
|
|
253
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
254
254
|
_console_tabsize: int = 8,
|
|
255
255
|
) -> None:
|
|
256
256
|
"""Will print a formatted log message:
|
|
@@ -271,8 +271,12 @@ class Console:
|
|
|
271
271
|
if format_linebreaks:
|
|
272
272
|
clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
|
|
273
273
|
prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
|
|
274
|
-
prompt_lst = (
|
|
275
|
-
|
|
274
|
+
prompt_lst = (
|
|
275
|
+
item for lst in prompt_lst for item in ([""] if lst == [] else (lst if isinstance(lst, list) else [lst]))
|
|
276
|
+
)
|
|
277
|
+
prompt = f"\n{' ' * title_len}\t".join(
|
|
278
|
+
Console.__add_back_removed_parts(list(prompt_lst), removals) # type: ignore[assignment]
|
|
279
|
+
)
|
|
276
280
|
else:
|
|
277
281
|
prompt = str(prompt)
|
|
278
282
|
if title == "":
|
|
@@ -328,8 +332,8 @@ class Console:
|
|
|
328
332
|
format_linebreaks: bool = True,
|
|
329
333
|
start: str = "",
|
|
330
334
|
end: str = "\n",
|
|
331
|
-
title_bg_color:
|
|
332
|
-
default_color:
|
|
335
|
+
title_bg_color: Rgba | Hexa = COLOR.yellow,
|
|
336
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
333
337
|
pause: bool = False,
|
|
334
338
|
exit: bool = False,
|
|
335
339
|
) -> None:
|
|
@@ -346,8 +350,8 @@ class Console:
|
|
|
346
350
|
format_linebreaks: bool = True,
|
|
347
351
|
start: str = "",
|
|
348
352
|
end: str = "\n",
|
|
349
|
-
title_bg_color:
|
|
350
|
-
default_color:
|
|
353
|
+
title_bg_color: Rgba | Hexa = COLOR.blue,
|
|
354
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
351
355
|
pause: bool = False,
|
|
352
356
|
exit: bool = False,
|
|
353
357
|
) -> None:
|
|
@@ -362,8 +366,8 @@ class Console:
|
|
|
362
366
|
format_linebreaks: bool = True,
|
|
363
367
|
start: str = "",
|
|
364
368
|
end: str = "\n",
|
|
365
|
-
title_bg_color:
|
|
366
|
-
default_color:
|
|
369
|
+
title_bg_color: Rgba | Hexa = COLOR.teal,
|
|
370
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
367
371
|
pause: bool = False,
|
|
368
372
|
exit: bool = False,
|
|
369
373
|
) -> None:
|
|
@@ -378,8 +382,8 @@ class Console:
|
|
|
378
382
|
format_linebreaks: bool = True,
|
|
379
383
|
start: str = "",
|
|
380
384
|
end: str = "\n",
|
|
381
|
-
title_bg_color:
|
|
382
|
-
default_color:
|
|
385
|
+
title_bg_color: Rgba | Hexa = COLOR.orange,
|
|
386
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
383
387
|
pause: bool = False,
|
|
384
388
|
exit: bool = False,
|
|
385
389
|
) -> None:
|
|
@@ -394,8 +398,8 @@ class Console:
|
|
|
394
398
|
format_linebreaks: bool = True,
|
|
395
399
|
start: str = "",
|
|
396
400
|
end: str = "\n",
|
|
397
|
-
title_bg_color:
|
|
398
|
-
default_color:
|
|
401
|
+
title_bg_color: Rgba | Hexa = COLOR.red,
|
|
402
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
399
403
|
pause: bool = False,
|
|
400
404
|
exit: bool = True,
|
|
401
405
|
reset_ansi=True,
|
|
@@ -411,8 +415,8 @@ class Console:
|
|
|
411
415
|
format_linebreaks: bool = True,
|
|
412
416
|
start: str = "",
|
|
413
417
|
end: str = "\n",
|
|
414
|
-
title_bg_color:
|
|
415
|
-
default_color:
|
|
418
|
+
title_bg_color: Rgba | Hexa = COLOR.magenta,
|
|
419
|
+
default_color: Rgba | Hexa = COLOR.text,
|
|
416
420
|
pause: bool = False,
|
|
417
421
|
exit: bool = True,
|
|
418
422
|
reset_ansi=True,
|
|
@@ -427,8 +431,8 @@ class Console:
|
|
|
427
431
|
*values: object,
|
|
428
432
|
start: str = "",
|
|
429
433
|
end: str = "\n",
|
|
430
|
-
box_bg_color: str |
|
|
431
|
-
default_color:
|
|
434
|
+
box_bg_color: str | Rgba | Hexa = "green",
|
|
435
|
+
default_color: Rgba | Hexa = "#000",
|
|
432
436
|
w_padding: int = 2,
|
|
433
437
|
w_full: bool = False,
|
|
434
438
|
) -> None:
|
|
@@ -443,7 +447,7 @@ class Console:
|
|
|
443
447
|
-----------------------------------------------------------------------------------
|
|
444
448
|
The box content can be formatted with special formatting codes. For more detailed
|
|
445
449
|
information about formatting codes, see `xx_format_codes` module documentation."""
|
|
446
|
-
lines = [line.rstrip() for val in values for line in val.splitlines()]
|
|
450
|
+
lines = [line.rstrip() for val in values for line in str(val).splitlines()]
|
|
447
451
|
unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
|
|
448
452
|
max_line_len = max(len(line) for line in unfmt_lines)
|
|
449
453
|
pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
|
|
@@ -466,7 +470,7 @@ class Console:
|
|
|
466
470
|
prompt: object = "Do you want to continue?",
|
|
467
471
|
start="",
|
|
468
472
|
end="\n",
|
|
469
|
-
default_color:
|
|
473
|
+
default_color: Rgba | Hexa = COLOR.cyan,
|
|
470
474
|
default_is_yes: bool = True,
|
|
471
475
|
) -> bool:
|
|
472
476
|
"""Ask a yes/no question.\n
|
|
@@ -488,7 +492,7 @@ class Console:
|
|
|
488
492
|
prompt: object = "",
|
|
489
493
|
start="",
|
|
490
494
|
end="\n",
|
|
491
|
-
default_color:
|
|
495
|
+
default_color: Rgba | Hexa = COLOR.cyan,
|
|
492
496
|
show_keybindings=True,
|
|
493
497
|
input_prefix=" ⤷ ",
|
|
494
498
|
reset_ansi=True,
|
|
@@ -511,7 +515,7 @@ class Console:
|
|
|
511
515
|
def _(event):
|
|
512
516
|
event.app.exit(result=event.app.current_buffer.document.text)
|
|
513
517
|
|
|
514
|
-
FormatCodes.print(start + prompt, default_color=default_color)
|
|
518
|
+
FormatCodes.print(start + str(prompt), default_color=default_color)
|
|
515
519
|
if show_keybindings:
|
|
516
520
|
FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
|
|
517
521
|
input_string = _prompt_toolkit.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
|
|
@@ -523,11 +527,11 @@ class Console:
|
|
|
523
527
|
prompt: object = "",
|
|
524
528
|
start="",
|
|
525
529
|
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,
|
|
530
|
+
default_color: Rgba | Hexa = COLOR.cyan,
|
|
531
|
+
allowed_chars: str = CHARS.all, # type: ignore[assignment]
|
|
532
|
+
min_len: Optional[int] = None,
|
|
533
|
+
max_len: Optional[int] = None,
|
|
534
|
+
mask_char: Optional[str] = None,
|
|
531
535
|
reset_ansi: bool = True,
|
|
532
536
|
) -> Optional[str]:
|
|
533
537
|
"""Acts like a standard Python `input()` with the advantage, that you can specify:
|
|
@@ -538,7 +542,7 @@ class Console:
|
|
|
538
542
|
---------------------------------------------------------------------------------------
|
|
539
543
|
The input can be formatted with special formatting codes. For more detailed
|
|
540
544
|
information about formatting codes, see the `xx_format_codes` module documentation."""
|
|
541
|
-
FormatCodes.print(start + prompt, default_color=default_color, end="")
|
|
545
|
+
FormatCodes.print(start + str(prompt), default_color=default_color, end="")
|
|
542
546
|
result = ""
|
|
543
547
|
select_all = False
|
|
544
548
|
last_line_count = 1
|
|
@@ -594,7 +598,8 @@ class Console:
|
|
|
594
598
|
|
|
595
599
|
def handle_character_input():
|
|
596
600
|
nonlocal result
|
|
597
|
-
if (allowed_chars == CHARS.all or event.name in allowed_chars) and
|
|
601
|
+
if event.name is not None and ((allowed_chars == CHARS.all or event.name in allowed_chars) and
|
|
602
|
+
(max_len is None or len(result) < max_len)):
|
|
598
603
|
result += event.name
|
|
599
604
|
update_display(Console.w)
|
|
600
605
|
|
|
@@ -615,7 +620,7 @@ class Console:
|
|
|
615
620
|
return None
|
|
616
621
|
elif event.name == "space":
|
|
617
622
|
handle_character_input()
|
|
618
|
-
elif len(event.name) == 1:
|
|
623
|
+
elif event.name is not None and len(event.name) == 1:
|
|
619
624
|
handle_character_input()
|
|
620
625
|
else:
|
|
621
626
|
select_all = False
|
|
@@ -626,12 +631,22 @@ class Console:
|
|
|
626
631
|
prompt: object = "Password: ",
|
|
627
632
|
start="",
|
|
628
633
|
end="\n",
|
|
629
|
-
default_color:
|
|
634
|
+
default_color: Rgba | Hexa = COLOR.cyan,
|
|
630
635
|
allowed_chars: str = CHARS.standard_ascii,
|
|
631
|
-
min_len: int = None,
|
|
632
|
-
max_len: int = None,
|
|
636
|
+
min_len: Optional[int] = None,
|
|
637
|
+
max_len: Optional[int] = None,
|
|
633
638
|
reset_ansi: bool = True,
|
|
634
|
-
) -> str:
|
|
639
|
+
) -> Optional[str]:
|
|
635
640
|
"""Password input (preset for `Console.restricted_input()`)
|
|
636
641
|
that always masks the entered characters with asterisks."""
|
|
637
|
-
return Console.restricted_input(
|
|
642
|
+
return Console.restricted_input(
|
|
643
|
+
prompt=prompt,
|
|
644
|
+
start=start,
|
|
645
|
+
end=end,
|
|
646
|
+
default_color=default_color,
|
|
647
|
+
allowed_chars=allowed_chars,
|
|
648
|
+
min_len=min_len,
|
|
649
|
+
max_len=max_len,
|
|
650
|
+
mask_char="*",
|
|
651
|
+
reset_ansi=reset_ansi,
|
|
652
|
+
)
|
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)
|
xulbux/xx_format_codes.py
CHANGED
|
@@ -152,10 +152,10 @@ Per default, you can also use `+` and `-` to get lighter and darker `default_col
|
|
|
152
152
|
|
|
153
153
|
from ._consts_ import ANSI
|
|
154
154
|
from .xx_string import String
|
|
155
|
-
from .xx_regex import Regex
|
|
156
|
-
from .xx_color import Color, rgba,
|
|
155
|
+
from .xx_regex import Regex, Match, Pattern
|
|
156
|
+
from .xx_color import Color, rgba, Rgba, Hexa
|
|
157
157
|
|
|
158
|
-
from typing import Optional
|
|
158
|
+
from typing import Optional
|
|
159
159
|
import ctypes as _ctypes
|
|
160
160
|
import regex as _rx
|
|
161
161
|
import sys as _sys
|
|
@@ -205,7 +205,7 @@ class FormatCodes:
|
|
|
205
205
|
@staticmethod
|
|
206
206
|
def print(
|
|
207
207
|
*values: object,
|
|
208
|
-
default_color:
|
|
208
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
209
209
|
brightness_steps: int = 20,
|
|
210
210
|
sep: str = " ",
|
|
211
211
|
end: str = "\n",
|
|
@@ -223,7 +223,7 @@ class FormatCodes:
|
|
|
223
223
|
@staticmethod
|
|
224
224
|
def input(
|
|
225
225
|
prompt: object = "",
|
|
226
|
-
default_color:
|
|
226
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
227
227
|
brightness_steps: int = 20,
|
|
228
228
|
reset_ansi: bool = False,
|
|
229
229
|
) -> str:
|
|
@@ -243,7 +243,7 @@ class FormatCodes:
|
|
|
243
243
|
@staticmethod
|
|
244
244
|
def to_ansi(
|
|
245
245
|
string: str,
|
|
246
|
-
default_color:
|
|
246
|
+
default_color: Optional[Rgba | Hexa] = None,
|
|
247
247
|
brightness_steps: int = 20,
|
|
248
248
|
_default_start: bool = True,
|
|
249
249
|
) -> str:
|
|
@@ -253,9 +253,9 @@ class FormatCodes:
|
|
|
253
253
|
`xx_format_codes` module documentation."""
|
|
254
254
|
if not isinstance(string, str):
|
|
255
255
|
string = str(string)
|
|
256
|
-
if Color.is_valid_rgba(default_color, False):
|
|
256
|
+
if default_color and Color.is_valid_rgba(default_color, False): # type: ignore[assignment]
|
|
257
257
|
use_default = True
|
|
258
|
-
elif Color.is_valid_hexa(default_color, False):
|
|
258
|
+
elif default_color and Color.is_valid_hexa(default_color, False): # type: ignore[assignment]
|
|
259
259
|
use_default, default_color = True, Color.to_rgba(default_color)
|
|
260
260
|
else:
|
|
261
261
|
use_default = False
|
|
@@ -264,9 +264,9 @@ class FormatCodes:
|
|
|
264
264
|
string = _COMPILED["*color"].sub(r"[\1default\2]", string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
|
|
265
265
|
|
|
266
266
|
def is_valid_color(color: str) -> bool:
|
|
267
|
-
return color in ANSI.color_map or Color.is_valid_rgba(color) or Color.is_valid_hexa(color)
|
|
267
|
+
return bool((color in ANSI.color_map) or Color.is_valid_rgba(color) or Color.is_valid_hexa(color))
|
|
268
268
|
|
|
269
|
-
def replace_keys(match:
|
|
269
|
+
def replace_keys(match: Match) -> str:
|
|
270
270
|
_formats = formats = match.group(1)
|
|
271
271
|
auto_reset_escaped = match.group(2)
|
|
272
272
|
auto_reset_txt = match.group(3)
|
|
@@ -280,8 +280,8 @@ class FormatCodes:
|
|
|
280
280
|
formats = FormatCodes.to_ansi(formats, default_color, brightness_steps, False)
|
|
281
281
|
format_keys = [k.strip() for k in formats.split("|") if k.strip()]
|
|
282
282
|
ansi_formats = [
|
|
283
|
-
r if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)) != k
|
|
284
|
-
for k in format_keys
|
|
283
|
+
r if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)) != k # type: ignore[assignment]
|
|
284
|
+
else f"[{k}]" for k in format_keys
|
|
285
285
|
]
|
|
286
286
|
if auto_reset_txt and not auto_reset_escaped:
|
|
287
287
|
reset_keys = []
|
|
@@ -306,8 +306,9 @@ class FormatCodes:
|
|
|
306
306
|
else:
|
|
307
307
|
reset_keys.append(f"_{k}")
|
|
308
308
|
ansi_resets = [
|
|
309
|
-
r for k in reset_keys
|
|
310
|
-
|
|
309
|
+
r for k in reset_keys
|
|
310
|
+
if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps) # type: ignore[assignment]
|
|
311
|
+
).startswith(f"{ANSI.char}{ANSI.start}")
|
|
311
312
|
]
|
|
312
313
|
else:
|
|
313
314
|
ansi_resets = []
|
|
@@ -325,7 +326,8 @@ class FormatCodes:
|
|
|
325
326
|
)
|
|
326
327
|
|
|
327
328
|
string = "\n".join(_COMPILED["formatting"].sub(replace_keys, line) for line in string.split("\n"))
|
|
328
|
-
return ((FormatCodes.__get_default_ansi(default_color) if _default_start else "")
|
|
329
|
+
return (((FormatCodes.__get_default_ansi(default_color) or "") if _default_start else "") # type: ignore[assignment]
|
|
330
|
+
+ string) if use_default else string
|
|
329
331
|
|
|
330
332
|
@staticmethod
|
|
331
333
|
def escape_ansi(ansi_string: str) -> str:
|
|
@@ -346,7 +348,7 @@ class FormatCodes:
|
|
|
346
348
|
if get_removals:
|
|
347
349
|
removals = []
|
|
348
350
|
|
|
349
|
-
def replacement(match:
|
|
351
|
+
def replacement(match: Match) -> str:
|
|
350
352
|
start_pos = match.start() - sum(len(removed) for _, removed in removals)
|
|
351
353
|
if removals and removals[-1][0] == start_pos:
|
|
352
354
|
start_pos = removals[-1][0]
|
|
@@ -394,8 +396,8 @@ class FormatCodes:
|
|
|
394
396
|
@staticmethod
|
|
395
397
|
def __get_default_ansi(
|
|
396
398
|
default_color: tuple,
|
|
397
|
-
format_key: str = None,
|
|
398
|
-
brightness_steps: int = None,
|
|
399
|
+
format_key: Optional[str] = None,
|
|
400
|
+
brightness_steps: Optional[int] = None,
|
|
399
401
|
_modifiers: tuple[str, str] = (ANSI.default_color_modifiers["lighten"], ANSI.default_color_modifiers["darken"]),
|
|
400
402
|
) -> Optional[str]:
|
|
401
403
|
"""Get the `default_color` and lighter/darker versions of it as ANSI code."""
|
|
@@ -403,8 +405,9 @@ class FormatCodes:
|
|
|
403
405
|
return (ANSI.seq_bg_color if format_key and _COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
|
|
404
406
|
*default_color[:3]
|
|
405
407
|
)
|
|
406
|
-
if not (format_key in _modifiers[0] or format_key in _modifiers[1]):
|
|
408
|
+
if format_key is None or not (format_key in _modifiers[0] or format_key in _modifiers[1]):
|
|
407
409
|
return None
|
|
410
|
+
assert format_key is not None
|
|
408
411
|
match = _COMPILED["modifier"].match(format_key)
|
|
409
412
|
if not match:
|
|
410
413
|
return None
|
|
@@ -415,23 +418,25 @@ class FormatCodes:
|
|
|
415
418
|
if adjust and adjust > 0:
|
|
416
419
|
modifiers = mod
|
|
417
420
|
break
|
|
421
|
+
new_rgb = default_color
|
|
418
422
|
if adjust == 0:
|
|
419
423
|
return None
|
|
420
424
|
elif modifiers in _modifiers[0]:
|
|
421
|
-
new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust)
|
|
425
|
+
new_rgb = tuple(Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust))
|
|
422
426
|
elif modifiers in _modifiers[1]:
|
|
423
|
-
new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust)
|
|
427
|
+
new_rgb = tuple(Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust))
|
|
424
428
|
return (ANSI.seq_bg_color if is_bg else ANSI.seq_color).format(*new_rgb[:3])
|
|
425
429
|
|
|
426
430
|
@staticmethod
|
|
427
|
-
def __get_replacement(format_key: str, default_color:
|
|
431
|
+
def __get_replacement(format_key: str, default_color: Optional[Rgba] = None, brightness_steps: int = 20) -> str:
|
|
428
432
|
"""Gives you the corresponding ANSI code for the given format key.
|
|
429
433
|
If `default_color` is not `None`, the text color will be `default_color` if all formats
|
|
430
434
|
are reset or you can get lighter or darker version of `default_color` (also as BG)"""
|
|
431
435
|
use_default = default_color and Color.is_valid_rgba(default_color, False)
|
|
436
|
+
_default_color = tuple(Color.to_rgba(default_color)) if use_default else () # type: ignore[assignment]
|
|
432
437
|
_format_key, format_key = format_key, FormatCodes.__normalize_key(format_key) # NORMALIZE KEY AND SAVE ORIGINAL
|
|
433
438
|
if use_default:
|
|
434
|
-
if new_default_color := FormatCodes.__get_default_ansi(
|
|
439
|
+
if new_default_color := FormatCodes.__get_default_ansi(_default_color, format_key, brightness_steps):
|
|
435
440
|
return new_default_color
|
|
436
441
|
for map_key in ANSI.codes_map:
|
|
437
442
|
if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
|
|
@@ -440,8 +445,8 @@ class FormatCodes:
|
|
|
440
445
|
v for k, v in ANSI.codes_map.items() if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
441
446
|
), None)
|
|
442
447
|
)
|
|
443
|
-
rgb_match =
|
|
444
|
-
hex_match =
|
|
448
|
+
rgb_match = _COMPILED["rgb"].match(format_key)
|
|
449
|
+
hex_match = _COMPILED["hex"].match(format_key)
|
|
445
450
|
try:
|
|
446
451
|
if rgb_match:
|
|
447
452
|
is_bg = rgb_match.group(1)
|
xulbux/xx_json.py
CHANGED
|
@@ -26,13 +26,15 @@ class Json:
|
|
|
26
26
|
if not json_file.endswith(".json"):
|
|
27
27
|
json_file += ".json"
|
|
28
28
|
file_path = Path.extend_or_make(json_file, prefer_script_dir=True)
|
|
29
|
+
if file_path is None:
|
|
30
|
+
raise FileNotFoundError(f"Could not find JSON file: {json_file}")
|
|
29
31
|
with open(file_path, "r") as f:
|
|
30
32
|
content = f.read()
|
|
31
33
|
try:
|
|
32
34
|
data = _json.loads(content)
|
|
33
35
|
except _json.JSONDecodeError as e:
|
|
34
36
|
raise ValueError(f"Error parsing JSON in '{file_path}': {str(e)}")
|
|
35
|
-
processed_data = Data.remove_comments(data, comment_start, comment_end)
|
|
37
|
+
processed_data = dict(Data.remove_comments(data, comment_start, comment_end))
|
|
36
38
|
if not processed_data:
|
|
37
39
|
raise ValueError(f"The JSON file '{file_path}' is empty or contains only comments.")
|
|
38
40
|
return (processed_data, data) if return_original else processed_data
|
|
@@ -135,24 +137,23 @@ class Json:
|
|
|
135
137
|
raise TypeError(f"Cannot navigate through {type(current).__name__}")
|
|
136
138
|
return data_obj
|
|
137
139
|
|
|
140
|
+
update = {}
|
|
138
141
|
for value_path, new_value in update_values.items():
|
|
139
142
|
try:
|
|
140
143
|
path_id = Data.get_path_id(
|
|
141
144
|
data=processed_data,
|
|
142
145
|
value_paths=value_path,
|
|
143
146
|
path_sep=path_sep,
|
|
144
|
-
ignore_not_found=True,
|
|
145
147
|
)
|
|
146
148
|
if path_id is not None:
|
|
147
|
-
if 'update' not in locals():
|
|
148
|
-
update = {}
|
|
149
149
|
update[path_id] = new_value
|
|
150
150
|
else:
|
|
151
|
+
keys = value_path.split(path_sep)
|
|
151
152
|
keys = value_path.split(path_sep)
|
|
152
153
|
data = create_nested_path(data, keys, new_value)
|
|
153
154
|
except Exception:
|
|
154
155
|
keys = value_path.split(path_sep)
|
|
155
156
|
data = create_nested_path(data, keys, new_value)
|
|
156
|
-
if
|
|
157
|
+
if "update" in locals() and update:
|
|
157
158
|
data = Data.set_value_by_path_id(data, update)
|
|
158
|
-
Json.create(json_file=json_file, data=data, force=True)
|
|
159
|
+
Json.create(json_file=json_file, data=dict(data), force=True)
|