xulbux 1.6.5__py3-none-any.whl → 1.6.7__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 +1 -1
- xulbux/_cli_.py +9 -9
- xulbux/_consts_.py +98 -62
- xulbux/xx_code.py +37 -42
- xulbux/xx_color.py +27 -55
- xulbux/xx_console.py +205 -110
- xulbux/xx_data.py +176 -128
- xulbux/xx_env_path.py +3 -7
- xulbux/xx_file.py +10 -4
- xulbux/xx_format_codes.py +26 -37
- xulbux/xx_json.py +2 -5
- xulbux/xx_path.py +33 -23
- xulbux/xx_regex.py +18 -20
- xulbux/xx_string.py +23 -76
- xulbux/xx_system.py +16 -19
- {xulbux-1.6.5.dist-info → xulbux-1.6.7.dist-info}/METADATA +92 -13
- xulbux-1.6.7.dist-info/RECORD +21 -0
- {xulbux-1.6.5.dist-info → xulbux-1.6.7.dist-info}/WHEEL +1 -1
- xulbux-1.6.5.dist-info/RECORD +0 -21
- {xulbux-1.6.5.dist-info → xulbux-1.6.7.dist-info}/LICENSE +0 -0
- {xulbux-1.6.5.dist-info → xulbux-1.6.7.dist-info}/entry_points.txt +0 -0
- {xulbux-1.6.5.dist-info → xulbux-1.6.7.dist-info}/top_level.txt +0 -0
xulbux/xx_data.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from ._consts_ import COLOR
|
|
2
|
+
from .xx_format_codes import FormatCodes
|
|
1
3
|
from .xx_string import String
|
|
2
4
|
|
|
3
|
-
from typing import TypeAlias, Union
|
|
5
|
+
from typing import TypeAlias, Optional, Union
|
|
6
|
+
import base64 as _base64
|
|
4
7
|
import math as _math
|
|
5
8
|
import re as _re
|
|
6
9
|
|
|
@@ -10,6 +13,32 @@ DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
|
|
|
10
13
|
|
|
11
14
|
class Data:
|
|
12
15
|
|
|
16
|
+
@staticmethod
|
|
17
|
+
def serialize_bytes(data: bytes | bytearray) -> dict[str, str]:
|
|
18
|
+
"""Converts bytes or bytearray to a JSON-compatible format (dictionary) with explicit keys."""
|
|
19
|
+
if isinstance(data, (bytes, bytearray)):
|
|
20
|
+
key = "bytearray" if isinstance(data, bytearray) else "bytes"
|
|
21
|
+
try:
|
|
22
|
+
return {key: data.decode("utf-8"), "encoding": "utf-8"}
|
|
23
|
+
except UnicodeDecodeError:
|
|
24
|
+
pass
|
|
25
|
+
return {key: _base64.b64encode(data).decode("utf-8"), "encoding": "base64"}
|
|
26
|
+
raise TypeError("Unsupported data type")
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def deserialize_bytes(obj: dict[str, str]) -> bytes | bytearray:
|
|
30
|
+
"""Converts a JSON-compatible bytes/bytearray format (dictionary) back to its original type."""
|
|
31
|
+
for key in ("bytes", "bytearray"):
|
|
32
|
+
if key in obj and "encoding" in obj:
|
|
33
|
+
if obj["encoding"] == "utf-8":
|
|
34
|
+
data = obj[key].encode("utf-8")
|
|
35
|
+
elif obj["encoding"] == "base64":
|
|
36
|
+
data = _base64.b64decode(obj[key].encode("utf-8"))
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("Unknown encoding method")
|
|
39
|
+
return bytearray(data) if key == "bytearray" else data
|
|
40
|
+
raise ValueError("Invalid serialized data")
|
|
41
|
+
|
|
13
42
|
@staticmethod
|
|
14
43
|
def chars_count(data: DataStructure) -> int:
|
|
15
44
|
"""The sum of all the characters amount including the keys in dictionaries."""
|
|
@@ -31,26 +60,17 @@ class Data:
|
|
|
31
60
|
if isinstance(data, dict):
|
|
32
61
|
return {
|
|
33
62
|
k: (
|
|
34
|
-
v
|
|
35
|
-
|
|
36
|
-
else Data.remove_empty_items(v, spaces_are_empty)
|
|
63
|
+
v if not isinstance(v,
|
|
64
|
+
(list, tuple, set, frozenset, dict)) else Data.remove_empty_items(v, spaces_are_empty)
|
|
37
65
|
)
|
|
38
|
-
for k, v in data.items()
|
|
39
|
-
if not String.is_empty(v, spaces_are_empty)
|
|
66
|
+
for k, v in data.items() if not String.is_empty(v, spaces_are_empty)
|
|
40
67
|
}
|
|
41
68
|
if isinstance(data, (list, tuple, set, frozenset)):
|
|
42
69
|
return type(data)(
|
|
43
|
-
item
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if not isinstance(item, (list, tuple, set, frozenset, dict))
|
|
48
|
-
else Data.remove_empty_items(item, spaces_are_empty)
|
|
49
|
-
)
|
|
50
|
-
for item in data
|
|
51
|
-
if not String.is_empty(item, spaces_are_empty)
|
|
52
|
-
)
|
|
53
|
-
if item not in ((), {}, set(), frozenset())
|
|
70
|
+
item for item in ((
|
|
71
|
+
item if not isinstance(item, (list, tuple, set, frozenset,
|
|
72
|
+
dict)) else Data.remove_empty_items(item, spaces_are_empty)
|
|
73
|
+
) for item in data if not String.is_empty(item, spaces_are_empty)) if item not in ((), {}, set(), frozenset())
|
|
54
74
|
)
|
|
55
75
|
return data
|
|
56
76
|
|
|
@@ -131,7 +151,7 @@ class Data:
|
|
|
131
151
|
rf"^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$"
|
|
132
152
|
)
|
|
133
153
|
|
|
134
|
-
def process_string(s: str) -> str
|
|
154
|
+
def process_string(s: str) -> Optional[str]:
|
|
135
155
|
if comment_end:
|
|
136
156
|
match = pattern.match(s)
|
|
137
157
|
if match:
|
|
@@ -144,7 +164,8 @@ class Data:
|
|
|
144
164
|
def process_item(item: any) -> any:
|
|
145
165
|
if isinstance(item, dict):
|
|
146
166
|
return {
|
|
147
|
-
k: v
|
|
167
|
+
k: v
|
|
168
|
+
for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
|
|
148
169
|
}
|
|
149
170
|
if isinstance(item, (list, tuple, set, frozenset)):
|
|
150
171
|
processed = (v for v in map(process_item, item) if v is not None)
|
|
@@ -174,9 +195,7 @@ class Data:
|
|
|
174
195
|
The paths from `ignore_paths` and the `path_sep` parameter work exactly the same way as for
|
|
175
196
|
the function `Data.get_path_id()`. See its documentation for more details."""
|
|
176
197
|
|
|
177
|
-
def process_ignore_paths(
|
|
178
|
-
ignore_paths: str | list[str],
|
|
179
|
-
) -> list[list[str]]:
|
|
198
|
+
def process_ignore_paths(ignore_paths: str | list[str], ) -> list[list[str]]:
|
|
180
199
|
if isinstance(ignore_paths, str):
|
|
181
200
|
ignore_paths = [ignore_paths]
|
|
182
201
|
return [path.split(path_sep) for path in ignore_paths if path]
|
|
@@ -187,9 +206,9 @@ class Data:
|
|
|
187
206
|
ignore_paths: list[list[str]],
|
|
188
207
|
current_path: list[str] = [],
|
|
189
208
|
) -> bool:
|
|
190
|
-
if any(current_path == path[:
|
|
209
|
+
if any(current_path == path[:len(current_path)] for path in ignore_paths):
|
|
191
210
|
return True
|
|
192
|
-
if type(d1)
|
|
211
|
+
if type(d1) is not type(d2):
|
|
193
212
|
return False
|
|
194
213
|
if isinstance(d1, dict):
|
|
195
214
|
if set(d1.keys()) != set(d2.keys()):
|
|
@@ -247,7 +266,7 @@ class Data:
|
|
|
247
266
|
If `ignore_not_found` is `True`, the function will return `None` if the value is not found
|
|
248
267
|
instead of raising an error."""
|
|
249
268
|
|
|
250
|
-
def process_path(path: str, data_obj:
|
|
269
|
+
def process_path(path: str, data_obj: DataStructure) -> Optional[str]:
|
|
251
270
|
keys = path.split(path_sep)
|
|
252
271
|
path_ids = []
|
|
253
272
|
max_id_length = 0
|
|
@@ -300,7 +319,7 @@ class Data:
|
|
|
300
319
|
The function will return the value (or key) from the path ID location, as long as the structure
|
|
301
320
|
of `data` hasn't changed since creating the path ID to that value."""
|
|
302
321
|
|
|
303
|
-
def get_nested(data:
|
|
322
|
+
def get_nested(data: DataStructure, path: list[int], get_key: bool) -> any:
|
|
304
323
|
parent = None
|
|
305
324
|
for i, idx in enumerate(path):
|
|
306
325
|
if isinstance(data, dict):
|
|
@@ -323,11 +342,7 @@ class Data:
|
|
|
323
342
|
return get_nested(data, Data.__sep_path_id(path_id), get_key)
|
|
324
343
|
|
|
325
344
|
@staticmethod
|
|
326
|
-
def set_value_by_path_id(
|
|
327
|
-
data: DataStructure,
|
|
328
|
-
update_values: str | list[str],
|
|
329
|
-
sep: str = "::",
|
|
330
|
-
) -> list | tuple | dict:
|
|
345
|
+
def set_value_by_path_id(data: DataStructure, update_values: str | list[str], sep: str = "::") -> list | tuple | dict:
|
|
331
346
|
"""Updates the value/s from `update_values` in the `data`.\n
|
|
332
347
|
--------------------------------------------------------------------------------
|
|
333
348
|
Input a list, tuple or dict as `data`, along with `update_values`, which is a
|
|
@@ -338,9 +353,7 @@ class Data:
|
|
|
338
353
|
The value from path ID will be changed to the new value, as long as the
|
|
339
354
|
structure of `data` hasn't changed since creating the path ID to that value."""
|
|
340
355
|
|
|
341
|
-
def update_nested(
|
|
342
|
-
data: list | tuple | set | frozenset | dict, path: list[int], value: any
|
|
343
|
-
) -> list | tuple | set | frozenset | dict:
|
|
356
|
+
def update_nested(data: DataStructure, path: list[int], value: any) -> DataStructure:
|
|
344
357
|
if len(path) == 1:
|
|
345
358
|
if isinstance(data, dict):
|
|
346
359
|
keys = list(data.keys())
|
|
@@ -364,11 +377,8 @@ class Data:
|
|
|
364
377
|
|
|
365
378
|
if isinstance(update_values, str):
|
|
366
379
|
update_values = [update_values]
|
|
367
|
-
valid_entries = [
|
|
368
|
-
|
|
369
|
-
for update_value in update_values
|
|
370
|
-
if len(parts := update_value.split(str(sep).strip())) == 2
|
|
371
|
-
]
|
|
380
|
+
valid_entries = [(parts[0].strip(), parts[1]) for update_value in update_values
|
|
381
|
+
if len(parts := update_value.split(str(sep).strip())) == 2]
|
|
372
382
|
if not valid_entries:
|
|
373
383
|
raise ValueError(f"No valid update_values found: {update_values}")
|
|
374
384
|
for path_id, new_val in valid_entries:
|
|
@@ -376,32 +386,6 @@ class Data:
|
|
|
376
386
|
data = update_nested(data, path, new_val)
|
|
377
387
|
return data
|
|
378
388
|
|
|
379
|
-
@staticmethod
|
|
380
|
-
def print(
|
|
381
|
-
data: DataStructure,
|
|
382
|
-
indent: int = 4,
|
|
383
|
-
compactness: int = 1,
|
|
384
|
-
max_width: int = 127,
|
|
385
|
-
sep: str = ", ",
|
|
386
|
-
end: str = "\n",
|
|
387
|
-
as_json: bool = False,
|
|
388
|
-
) -> None:
|
|
389
|
-
"""Print nicely formatted data structures.\n
|
|
390
|
-
------------------------------------------------------------------------------
|
|
391
|
-
The indentation spaces-amount can be set with with `indent`.
|
|
392
|
-
There are three different levels of `compactness`:
|
|
393
|
-
- `0` expands everything possible
|
|
394
|
-
- `1` only expands if there's other lists, tuples or dicts inside of data or,
|
|
395
|
-
if the data's content is longer than `max_width`
|
|
396
|
-
- `2` keeps everything collapsed (all on one line)\n
|
|
397
|
-
------------------------------------------------------------------------------
|
|
398
|
-
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
399
|
-
print(
|
|
400
|
-
Data.to_str(data, indent, compactness, sep, max_width, as_json),
|
|
401
|
-
end=end,
|
|
402
|
-
flush=True,
|
|
403
|
-
)
|
|
404
|
-
|
|
405
389
|
@staticmethod
|
|
406
390
|
def to_str(
|
|
407
391
|
data: DataStructure,
|
|
@@ -410,6 +394,7 @@ class Data:
|
|
|
410
394
|
max_width: int = 127,
|
|
411
395
|
sep: str = ", ",
|
|
412
396
|
as_json: bool = False,
|
|
397
|
+
_syntax_highlighting: dict[str, str] | bool = False,
|
|
413
398
|
) -> str:
|
|
414
399
|
"""Get nicely formatted data structure-strings.\n
|
|
415
400
|
------------------------------------------------------------------------------
|
|
@@ -421,106 +406,169 @@ class Data:
|
|
|
421
406
|
- `2` keeps everything collapsed (all on one line)\n
|
|
422
407
|
------------------------------------------------------------------------------
|
|
423
408
|
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
409
|
+
if syntax_hl := _syntax_highlighting not in (None, False):
|
|
410
|
+
if _syntax_highlighting is True:
|
|
411
|
+
_syntax_highlighting = {}
|
|
412
|
+
elif not isinstance(_syntax_highlighting, dict):
|
|
413
|
+
raise TypeError(f"Expected 'syntax_highlighting' to be a dict or bool. Got: {type(_syntax_highlighting)}")
|
|
414
|
+
_syntax_hl = {
|
|
415
|
+
"str": (f"[{COLOR.blue}]", "[_c]"),
|
|
416
|
+
"number": (f"[{COLOR.magenta}]", "[_c]"),
|
|
417
|
+
"literal": (f"[{COLOR.cyan}]", "[_c]"),
|
|
418
|
+
"type": (f"[i|{COLOR.lightblue}]", "[_i|_c]"),
|
|
419
|
+
"punctuation": (f"[{COLOR.darkgray}]", "[_c]"),
|
|
420
|
+
}
|
|
421
|
+
_syntax_hl.update({
|
|
422
|
+
k: [f"[{v}]", "[_]"] if k in _syntax_hl and v not in ("", None) else ["", ""]
|
|
423
|
+
for k, v in _syntax_highlighting.items()
|
|
424
|
+
})
|
|
425
|
+
sep = f"{_syntax_hl['punctuation'][0]}{sep}{_syntax_hl['punctuation'][1]}"
|
|
426
|
+
punct_map = {"(": ("/(", "("), **{char: char for char in "'\":)[]{}"}}
|
|
427
|
+
punct = {
|
|
428
|
+
k: ((f"{_syntax_hl['punctuation'][0]}{v[0]}{_syntax_hl['punctuation'][1]}" if syntax_hl else v[1])
|
|
429
|
+
if isinstance(v, (list, tuple)) else
|
|
430
|
+
(f"{_syntax_hl['punctuation'][0]}{v}{_syntax_hl['punctuation'][1]}" if syntax_hl else v))
|
|
431
|
+
for k, v in punct_map.items()
|
|
432
|
+
}
|
|
424
433
|
|
|
425
|
-
def format_value(value: any, current_indent: int) -> str:
|
|
426
|
-
if isinstance(value, dict):
|
|
434
|
+
def format_value(value: any, current_indent: int = None) -> str:
|
|
435
|
+
if current_indent is not None and isinstance(value, dict):
|
|
427
436
|
return format_dict(value, current_indent + indent)
|
|
428
|
-
elif hasattr(value, "__dict__"):
|
|
437
|
+
elif current_indent is not None and hasattr(value, "__dict__"):
|
|
429
438
|
return format_dict(value.__dict__, current_indent + indent)
|
|
430
|
-
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
439
|
+
elif current_indent is not None and isinstance(value, (list, tuple, set, frozenset)):
|
|
431
440
|
return format_sequence(value, current_indent + indent)
|
|
441
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
442
|
+
obj_dict = Data.serialize_bytes(value)
|
|
443
|
+
return (
|
|
444
|
+
format_dict(obj_dict, current_indent + indent) if as_json else (
|
|
445
|
+
f"{_syntax_hl['type'][0]}{(k := next(iter(obj_dict)))}{_syntax_hl['type'][1]}"
|
|
446
|
+
+ format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent) if syntax_hl else
|
|
447
|
+
(k := next(iter(obj_dict)))
|
|
448
|
+
+ format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent)
|
|
449
|
+
)
|
|
450
|
+
)
|
|
432
451
|
elif isinstance(value, bool):
|
|
433
|
-
|
|
452
|
+
val = str(value).lower() if as_json else str(value)
|
|
453
|
+
return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
|
|
434
454
|
elif isinstance(value, (int, float)):
|
|
435
|
-
|
|
455
|
+
val = "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
|
|
456
|
+
return f"{_syntax_hl['number'][0]}{val}{_syntax_hl['number'][1]}" if syntax_hl else val
|
|
436
457
|
elif isinstance(value, complex):
|
|
437
|
-
return
|
|
458
|
+
return (
|
|
459
|
+
format_value(str(value).strip("()")) if as_json else (
|
|
460
|
+
f"{_syntax_hl['type'][0]}complex{_syntax_hl['type'][1]}"
|
|
461
|
+
+ format_sequence((value.real, value.imag), current_indent + indent)
|
|
462
|
+
if syntax_hl else f"complex{format_sequence((value.real, value.imag), current_indent + indent)}"
|
|
463
|
+
)
|
|
464
|
+
)
|
|
438
465
|
elif value is None:
|
|
439
|
-
|
|
466
|
+
val = "null" if as_json else "None"
|
|
467
|
+
return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
|
|
440
468
|
else:
|
|
441
|
-
return
|
|
469
|
+
return ((
|
|
470
|
+
punct['"'] + _syntax_hl["str"][0] + String.escape(str(value), '"') + _syntax_hl["str"][1]
|
|
471
|
+
+ punct['"'] if syntax_hl else punct['"'] + String.escape(str(value), '"') + punct['"']
|
|
472
|
+
) if as_json else (
|
|
473
|
+
punct["'"] + _syntax_hl["str"][0] + String.escape(str(value), "'") + _syntax_hl["str"][1]
|
|
474
|
+
+ punct["'"] if syntax_hl else punct["'"] + String.escape(str(value), "'") + punct["'"]
|
|
475
|
+
))
|
|
442
476
|
|
|
443
477
|
def should_expand(seq: list | tuple | dict) -> bool:
|
|
444
478
|
if compactness == 0:
|
|
445
479
|
return True
|
|
446
480
|
if compactness == 2:
|
|
447
481
|
return False
|
|
448
|
-
|
|
482
|
+
complex_types = (list, tuple, dict, set, frozenset) + ((bytes, bytearray) if as_json else ())
|
|
483
|
+
complex_items = sum(1 for item in seq if isinstance(item, complex_types))
|
|
449
484
|
return (
|
|
450
|
-
complex_items > 1
|
|
451
|
-
|
|
452
|
-
or Data.chars_count(seq) + (len(seq) * len(sep)) > max_width
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
def format_key(k: any) -> str:
|
|
456
|
-
return (
|
|
457
|
-
'"' + String.escape(str(k), '"') + '"'
|
|
458
|
-
if as_json
|
|
459
|
-
else ("'" + String.escape(str(k), "'") + "'" if isinstance(k, str) else str(k))
|
|
485
|
+
complex_items > 1 or (complex_items == 1 and len(seq) > 1) or Data.chars_count(seq) +
|
|
486
|
+
(len(seq) * len(sep)) > max_width
|
|
460
487
|
)
|
|
461
488
|
|
|
462
489
|
def format_dict(d: dict, current_indent: int) -> str:
|
|
463
490
|
if not d or compactness == 2:
|
|
464
|
-
return
|
|
491
|
+
return (
|
|
492
|
+
punct["{"]
|
|
493
|
+
+ sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
|
|
494
|
+
for k, v in d.items()) + punct["}"]
|
|
495
|
+
)
|
|
465
496
|
if not should_expand(d.values()):
|
|
466
|
-
return
|
|
497
|
+
return (
|
|
498
|
+
punct["{"]
|
|
499
|
+
+ sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
|
|
500
|
+
for k, v in d.items()) + punct["}"]
|
|
501
|
+
)
|
|
467
502
|
items = []
|
|
468
|
-
for
|
|
469
|
-
formatted_value = format_value(
|
|
470
|
-
items.append(f'
|
|
471
|
-
return "{\n" + "
|
|
503
|
+
for k, val in d.items():
|
|
504
|
+
formatted_value = format_value(val, current_indent)
|
|
505
|
+
items.append(f"{' ' * (current_indent + indent)}{format_value(k)}{punct[':']} {formatted_value}")
|
|
506
|
+
return punct["{"] + "\n" + f"{sep}\n".join(items) + f"\n{' ' * current_indent}" + punct["}"]
|
|
472
507
|
|
|
473
508
|
def format_sequence(seq, current_indent: int) -> str:
|
|
474
509
|
if as_json:
|
|
475
510
|
seq = list(seq)
|
|
476
511
|
if not seq or compactness == 2:
|
|
477
512
|
return (
|
|
478
|
-
"[" + sep.join(format_value(item, current_indent)
|
|
479
|
-
|
|
480
|
-
|
|
513
|
+
punct["["] + sep.join(format_value(item, current_indent)
|
|
514
|
+
for item in seq) + punct["]"] if isinstance(seq, list) else punct["("]
|
|
515
|
+
+ sep.join(format_value(item, current_indent) for item in seq) + punct[")"]
|
|
481
516
|
)
|
|
482
517
|
if not should_expand(seq):
|
|
483
518
|
return (
|
|
484
|
-
"[" + sep.join(format_value(item, current_indent)
|
|
485
|
-
|
|
486
|
-
|
|
519
|
+
punct["["] + sep.join(format_value(item, current_indent)
|
|
520
|
+
for item in seq) + punct["]"] if isinstance(seq, list) else punct["("]
|
|
521
|
+
+ sep.join(format_value(item, current_indent) for item in seq) + punct[")"]
|
|
487
522
|
)
|
|
488
523
|
items = [format_value(item, current_indent) for item in seq]
|
|
489
|
-
formatted_items = "
|
|
524
|
+
formatted_items = f"{sep}\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
|
|
490
525
|
if isinstance(seq, list):
|
|
491
|
-
return "[\n
|
|
526
|
+
return f"{punct['[']}\n{formatted_items}\n{' ' * current_indent}{punct[']']}"
|
|
492
527
|
else:
|
|
493
|
-
return "(\n
|
|
528
|
+
return f"{punct['(']}\n{formatted_items}\n{' ' * current_indent}{punct[')']}"
|
|
494
529
|
|
|
495
|
-
return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
|
|
530
|
+
return _re.sub(r"\s+(?=\n)", "", format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0))
|
|
496
531
|
|
|
497
532
|
@staticmethod
|
|
498
|
-
def
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
533
|
+
def print(
|
|
534
|
+
data: DataStructure,
|
|
535
|
+
indent: int = 4,
|
|
536
|
+
compactness: int = 1,
|
|
537
|
+
max_width: int = 127,
|
|
538
|
+
sep: str = ", ",
|
|
539
|
+
end: str = "\n",
|
|
540
|
+
as_json: bool = False,
|
|
541
|
+
syntax_highlighting: dict[str, str] | bool = {},
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Print nicely formatted data structures.\n
|
|
544
|
+
------------------------------------------------------------------------------
|
|
545
|
+
The indentation spaces-amount can be set with with `indent`.
|
|
546
|
+
There are three different levels of `compactness`:
|
|
547
|
+
- `0` expands everything possible
|
|
548
|
+
- `1` only expands if there's other lists, tuples or dicts inside of data or,
|
|
549
|
+
if the data's content is longer than `max_width`
|
|
550
|
+
- `2` keeps everything collapsed (all on one line)\n
|
|
551
|
+
------------------------------------------------------------------------------
|
|
552
|
+
If `as_json` is set to `True`, the output will be in valid JSON format.\n
|
|
553
|
+
------------------------------------------------------------------------------
|
|
554
|
+
The `syntax_highlighting` parameter is a dictionary with 5 keys for each part
|
|
555
|
+
of the data. The key's values are the formatting codes to apply to this data
|
|
556
|
+
part. The formatting can be changed by simply adding the key with the new
|
|
557
|
+
value inside the `syntax_highlighting` dictionary.\n
|
|
558
|
+
The keys with their default values are:
|
|
559
|
+
- `str: COLOR.blue`
|
|
560
|
+
- `number: COLOR.magenta`
|
|
561
|
+
- `literal: COLOR.cyan`
|
|
562
|
+
- `type: "i|" + COLOR.lightblue`
|
|
563
|
+
- `punctuation: COLOR.darkgray`\n
|
|
564
|
+
For no syntax highlighting, set `syntax_highlighting` to `False` or `None`.\n
|
|
565
|
+
------------------------------------------------------------------------------
|
|
566
|
+
For more detailed information about formatting codes, see `xx_format_codes`
|
|
567
|
+
module documentation."""
|
|
568
|
+
FormatCodes.print(
|
|
569
|
+
Data.to_str(data, indent, compactness, max_width, sep, as_json, syntax_highlighting),
|
|
570
|
+
end=end,
|
|
571
|
+
)
|
|
524
572
|
|
|
525
573
|
@staticmethod
|
|
526
574
|
def __sep_path_id(path_id: str) -> list[int]:
|
|
@@ -528,4 +576,4 @@ class Data:
|
|
|
528
576
|
raise ValueError(f"Invalid path ID: {path_id}")
|
|
529
577
|
id_part_len = int(path_id.split(">")[0])
|
|
530
578
|
path_ids_str = path_id.split(">")[1]
|
|
531
|
-
return [int(path_ids_str[i
|
|
579
|
+
return [int(path_ids_str[i:i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
|
xulbux/xx_env_path.py
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Functions for modifying and checking the systems environment-variables
|
|
3
|
-
- `EnvPath.paths()`
|
|
4
|
-
- `EnvPath.has_path()`
|
|
5
|
-
- `EnvPath.add_path()`
|
|
6
|
-
- `EnvPath.remove_path()`
|
|
2
|
+
Functions for modifying and checking the systems environment-variables (especially the PATH object).
|
|
7
3
|
"""
|
|
8
4
|
|
|
9
5
|
from .xx_path import Path
|
|
@@ -26,7 +22,7 @@ class EnvPath:
|
|
|
26
22
|
if cwd:
|
|
27
23
|
path = _os.getcwd()
|
|
28
24
|
elif base_dir:
|
|
29
|
-
path = Path.
|
|
25
|
+
path = Path.script_dir
|
|
30
26
|
elif path is None:
|
|
31
27
|
raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
|
|
32
28
|
paths = EnvPath.paths(as_list=True)
|
|
@@ -66,7 +62,7 @@ class EnvPath:
|
|
|
66
62
|
if cwd:
|
|
67
63
|
path = _os.getcwd()
|
|
68
64
|
elif base_dir:
|
|
69
|
-
path = Path.
|
|
65
|
+
path = Path.script_dir
|
|
70
66
|
elif path is None:
|
|
71
67
|
raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
|
|
72
68
|
return _os.path.normpath(path)
|
xulbux/xx_file.py
CHANGED
|
@@ -4,6 +4,10 @@ from .xx_path import Path
|
|
|
4
4
|
import os as _os
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class SameContentFileExistsError(FileExistsError):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
7
11
|
class File:
|
|
8
12
|
|
|
9
13
|
@staticmethod
|
|
@@ -29,14 +33,16 @@ class File:
|
|
|
29
33
|
force: bool = False,
|
|
30
34
|
) -> str:
|
|
31
35
|
"""Create a file with ot without content.\n
|
|
32
|
-
|
|
33
|
-
The function will throw a `FileExistsError` if
|
|
36
|
+
----------------------------------------------------------------------
|
|
37
|
+
The function will throw a `FileExistsError` if a file with the same
|
|
38
|
+
name already exists and a `SameContentFileExistsError` if a file with
|
|
39
|
+
the same name and content already exists.
|
|
34
40
|
To always overwrite the file, set the `force` parameter to `True`."""
|
|
35
41
|
if _os.path.exists(file) and not force:
|
|
36
42
|
with open(file, "r", encoding="utf-8") as existing_file:
|
|
37
43
|
existing_content = existing_file.read()
|
|
38
44
|
if existing_content == content:
|
|
39
|
-
raise
|
|
45
|
+
raise SameContentFileExistsError("Already created this file. (nothing changed)")
|
|
40
46
|
raise FileExistsError("File already exists.")
|
|
41
47
|
with open(file, "w", encoding="utf-8") as f:
|
|
42
48
|
f.write(content)
|
|
@@ -62,4 +68,4 @@ class File:
|
|
|
62
68
|
try:
|
|
63
69
|
return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
|
|
64
70
|
except FileNotFoundError:
|
|
65
|
-
return _os.path.join(Path.
|
|
71
|
+
return _os.path.join(Path.script_dir, file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)
|
xulbux/xx_format_codes.py
CHANGED
|
@@ -153,31 +153,33 @@ Per default, you can also use `+` and `-` to get lighter and darker `default_col
|
|
|
153
153
|
from ._consts_ import ANSI
|
|
154
154
|
from .xx_string import String
|
|
155
155
|
from .xx_regex import Regex
|
|
156
|
-
from .xx_color import
|
|
156
|
+
from .xx_color import Color, rgba, hexa
|
|
157
157
|
|
|
158
|
+
from typing import Optional, Pattern
|
|
158
159
|
import ctypes as _ctypes
|
|
159
160
|
import regex as _rx
|
|
160
161
|
import sys as _sys
|
|
161
162
|
import re as _re
|
|
162
163
|
|
|
163
|
-
_CONSOLE_ANSI_CONFIGURED = False
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
_CONSOLE_ANSI_CONFIGURED: bool = False
|
|
166
|
+
|
|
167
|
+
_PREFIX: dict[str, set[str]] = {
|
|
166
168
|
"BG": {"background", "bg"},
|
|
167
169
|
"BR": {"bright", "br"},
|
|
168
170
|
}
|
|
169
|
-
_PREFIX_RX = {
|
|
171
|
+
_PREFIX_RX: dict[str, str] = {
|
|
170
172
|
"BG": rf"(?:{'|'.join(_PREFIX['BG'])})\s*:",
|
|
171
173
|
"BR": rf"(?:{'|'.join(_PREFIX['BR'])})\s*:",
|
|
172
174
|
}
|
|
173
|
-
_COMPILED = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
175
|
+
_COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
174
176
|
"*": _re.compile(r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]"),
|
|
175
177
|
"*color": _re.compile(r"\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]"),
|
|
176
178
|
"ansi_seq": _re.compile(ANSI.char + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
|
|
177
179
|
"formatting": _rx.compile(
|
|
178
|
-
Regex.brackets("[", "]", is_group=True)
|
|
180
|
+
Regex.brackets("[", "]", is_group=True, ignore_in_strings=False)
|
|
179
181
|
+ r"(?:\s*([/\\]?)\s*"
|
|
180
|
-
+ Regex.brackets("(", ")", is_group=True, ignore_in_strings=False)
|
|
182
|
+
+ Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False)
|
|
181
183
|
+ r")?"
|
|
182
184
|
),
|
|
183
185
|
"bg?_default": _re.compile(r"(?i)((?:" + _PREFIX_RX["BG"] + r")?)\s*default"),
|
|
@@ -294,33 +296,25 @@ class FormatCodes:
|
|
|
294
296
|
reset_keys.append("_bg")
|
|
295
297
|
break
|
|
296
298
|
elif is_valid_color(k) or any(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
):
|
|
299
|
+
k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon):])
|
|
300
|
+
for prefix in _PREFIX["BR"]):
|
|
300
301
|
reset_keys.append("_color")
|
|
301
302
|
else:
|
|
302
303
|
reset_keys.append(f"_{k}")
|
|
303
304
|
ansi_resets = [
|
|
304
|
-
r
|
|
305
|
-
|
|
306
|
-
if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(
|
|
307
|
-
f"{ANSI.char}{ANSI.start}"
|
|
308
|
-
)
|
|
305
|
+
r for k in reset_keys if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)
|
|
306
|
+
).startswith(f"{ANSI.char}{ANSI.start}")
|
|
309
307
|
]
|
|
310
308
|
else:
|
|
311
309
|
ansi_resets = []
|
|
312
310
|
if not (len(ansi_formats) == 1 and ansi_formats[0].count(f"{ANSI.char}{ANSI.start}") >= 1) and not all(
|
|
313
|
-
|
|
314
|
-
):
|
|
311
|
+
f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats):
|
|
315
312
|
return match.group(0)
|
|
316
313
|
return (
|
|
317
|
-
"".join(ansi_formats)
|
|
318
|
-
+ (
|
|
314
|
+
"".join(ansi_formats) + (
|
|
319
315
|
f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
|
|
320
|
-
if escaped and auto_reset_txt
|
|
321
|
-
|
|
322
|
-
)
|
|
323
|
-
+ ("" if escaped else "".join(ansi_resets))
|
|
316
|
+
if escaped and auto_reset_txt else auto_reset_txt if auto_reset_txt else ""
|
|
317
|
+
) + ("" if escaped else "".join(ansi_resets))
|
|
324
318
|
)
|
|
325
319
|
|
|
326
320
|
string = "\n".join(_COMPILED["formatting"].sub(replace_keys, line) for line in string.split("\n"))
|
|
@@ -360,7 +354,7 @@ class FormatCodes:
|
|
|
360
354
|
format_key: str = None,
|
|
361
355
|
brightness_steps: int = None,
|
|
362
356
|
_modifiers: tuple[str, str] = (ANSI.default_color_modifiers["lighten"], ANSI.default_color_modifiers["darken"]),
|
|
363
|
-
) -> str
|
|
357
|
+
) -> Optional[str]:
|
|
364
358
|
"""Get the `default_color` and lighter/darker versions of it as ANSI code."""
|
|
365
359
|
if not brightness_steps or (format_key and _COMPILED["bg?_default"].search(format_key)):
|
|
366
360
|
return (ANSI.seq_bg_color if format_key and _COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
|
|
@@ -399,14 +393,9 @@ class FormatCodes:
|
|
|
399
393
|
for map_key in ANSI.codes_map:
|
|
400
394
|
if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
|
|
401
395
|
return ANSI.seq().format(
|
|
402
|
-
next(
|
|
403
|
-
(
|
|
404
|
-
|
|
405
|
-
for k, v in ANSI.codes_map.items()
|
|
406
|
-
if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
407
|
-
),
|
|
408
|
-
None,
|
|
409
|
-
)
|
|
396
|
+
next((
|
|
397
|
+
v for k, v in ANSI.codes_map.items() if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
398
|
+
), None)
|
|
410
399
|
)
|
|
411
400
|
rgb_match = _re.match(_COMPILED["rgb"], format_key)
|
|
412
401
|
hex_match = _re.match(_COMPILED["hex"], format_key)
|
|
@@ -421,8 +410,7 @@ class FormatCodes:
|
|
|
421
410
|
rgb = Color.to_rgba(hex_match.group(2))
|
|
422
411
|
return (
|
|
423
412
|
ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2])
|
|
424
|
-
if is_bg
|
|
425
|
-
else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
|
|
413
|
+
if is_bg else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
|
|
426
414
|
)
|
|
427
415
|
except Exception:
|
|
428
416
|
pass
|
|
@@ -433,10 +421,11 @@ class FormatCodes:
|
|
|
433
421
|
"""Normalizes the given format key."""
|
|
434
422
|
k_parts = format_key.replace(" ", "").lower().split(":")
|
|
435
423
|
prefix_str = "".join(
|
|
436
|
-
f"{prefix_key.lower()}:"
|
|
437
|
-
for prefix_key, prefix_values in _PREFIX.items()
|
|
424
|
+
f"{prefix_key.lower()}:" for prefix_key, prefix_values in _PREFIX.items()
|
|
438
425
|
if any(k_part in prefix_values for k_part in k_parts)
|
|
439
426
|
)
|
|
440
427
|
return prefix_str + ":".join(
|
|
441
|
-
part for part in k_parts if part not in {val
|
|
428
|
+
part for part in k_parts if part not in {val
|
|
429
|
+
for values in _PREFIX.values()
|
|
430
|
+
for val in values}
|
|
442
431
|
)
|