xulbux 1.6.8__py3-none-any.whl → 1.6.9__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 +4 -36
- xulbux/_cli_.py +4 -4
- xulbux/xx_code.py +61 -45
- xulbux/xx_color.py +91 -58
- xulbux/xx_console.py +98 -39
- xulbux/xx_data.py +60 -53
- xulbux/xx_env_path.py +0 -4
- xulbux/xx_file.py +22 -26
- xulbux/xx_format_codes.py +25 -8
- xulbux/xx_json.py +105 -50
- xulbux/xx_path.py +71 -21
- xulbux/xx_regex.py +5 -9
- xulbux/xx_string.py +4 -1
- xulbux/xx_system.py +1 -5
- {xulbux-1.6.8.dist-info → xulbux-1.6.9.dist-info}/METADATA +18 -39
- xulbux-1.6.9.dist-info/RECORD +21 -0
- {xulbux-1.6.8.dist-info → xulbux-1.6.9.dist-info}/WHEEL +1 -1
- xulbux-1.6.8.dist-info/RECORD +0 -21
- {xulbux-1.6.8.dist-info → xulbux-1.6.9.dist-info}/entry_points.txt +0 -0
- {xulbux-1.6.8.dist-info → xulbux-1.6.9.dist-info/licenses}/LICENSE +0 -0
- {xulbux-1.6.8.dist-info → xulbux-1.6.9.dist-info}/top_level.txt +0 -0
xulbux/xx_console.py
CHANGED
|
@@ -11,7 +11,7 @@ from .xx_string import String
|
|
|
11
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
|
|
14
|
+
from typing import Optional, Any
|
|
15
15
|
import prompt_toolkit as _prompt_toolkit
|
|
16
16
|
import pyperclip as _pyperclip
|
|
17
17
|
import keyboard as _keyboard
|
|
@@ -22,64 +22,83 @@ import sys as _sys
|
|
|
22
22
|
import os as _os
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
# YAPF: disable
|
|
26
25
|
class _ConsoleWidth:
|
|
26
|
+
|
|
27
27
|
def __get__(self, obj, owner=None):
|
|
28
28
|
return _os.get_terminal_size().columns
|
|
29
29
|
|
|
30
|
+
|
|
30
31
|
class _ConsoleHeight:
|
|
32
|
+
|
|
31
33
|
def __get__(self, obj, owner=None):
|
|
32
34
|
return _os.get_terminal_size().lines
|
|
33
35
|
|
|
36
|
+
|
|
34
37
|
class _ConsoleSize:
|
|
38
|
+
|
|
35
39
|
def __get__(self, obj, owner=None):
|
|
36
40
|
size = _os.get_terminal_size()
|
|
37
41
|
return (size.columns, size.lines)
|
|
38
42
|
|
|
43
|
+
|
|
39
44
|
class _ConsoleUser:
|
|
45
|
+
|
|
40
46
|
def __get__(self, obj, owner=None):
|
|
41
47
|
return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
|
|
42
48
|
|
|
49
|
+
|
|
43
50
|
class ArgResult:
|
|
44
51
|
"""Exists: if the argument was found or not\n
|
|
45
52
|
Value: the value from behind the found argument"""
|
|
46
|
-
|
|
53
|
+
|
|
54
|
+
def __init__(self, exists: bool, value: Any):
|
|
47
55
|
self.exists = exists
|
|
48
56
|
self.value = value
|
|
57
|
+
|
|
49
58
|
def __bool__(self):
|
|
50
59
|
return self.exists
|
|
51
60
|
|
|
61
|
+
|
|
52
62
|
class Args:
|
|
53
63
|
"""Stores found command arguments under their aliases with their results."""
|
|
64
|
+
|
|
54
65
|
def __init__(self, **kwargs):
|
|
55
66
|
for key, value in kwargs.items():
|
|
56
67
|
if not key.isidentifier():
|
|
57
68
|
raise TypeError(f"Argument alias '{key}' is invalid. It must be a valid Python variable name.")
|
|
58
69
|
setattr(self, key, ArgResult(**value))
|
|
70
|
+
|
|
59
71
|
def __len__(self):
|
|
60
72
|
return len(vars(self))
|
|
73
|
+
|
|
61
74
|
def __contains__(self, key):
|
|
62
75
|
return hasattr(self, key)
|
|
76
|
+
|
|
63
77
|
def __getitem__(self, key):
|
|
64
78
|
if isinstance(key, int):
|
|
65
79
|
return list(self.__iter__())[key]
|
|
66
80
|
return getattr(self, key)
|
|
81
|
+
|
|
67
82
|
def __iter__(self):
|
|
68
83
|
for key, value in vars(self).items():
|
|
69
84
|
yield (key, {"exists": value.exists, "value": value.value})
|
|
70
|
-
|
|
85
|
+
|
|
86
|
+
def dict(self) -> dict[str, dict[str, Any]]:
|
|
71
87
|
"""Returns the arguments as a dictionary."""
|
|
72
88
|
return {k: {"exists": v.exists, "value": v.value} for k, v in vars(self).items()}
|
|
89
|
+
|
|
73
90
|
def keys(self):
|
|
74
91
|
"""Returns the argument aliases as `dict_keys([...])`."""
|
|
75
92
|
return vars(self).keys()
|
|
93
|
+
|
|
76
94
|
def values(self):
|
|
77
95
|
"""Returns the argument results as `dict_values([...])`."""
|
|
78
96
|
return vars(self).values()
|
|
97
|
+
|
|
79
98
|
def items(self):
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
"""Yields tuples of `(alias, {'exists': bool, 'value': Any})`."""
|
|
100
|
+
for key, value in self.__iter__():
|
|
101
|
+
yield (key, value)
|
|
83
102
|
|
|
84
103
|
|
|
85
104
|
class Console:
|
|
@@ -95,33 +114,50 @@ class Console:
|
|
|
95
114
|
"""The name of the current user."""
|
|
96
115
|
|
|
97
116
|
@staticmethod
|
|
98
|
-
def get_args(
|
|
117
|
+
def get_args(
|
|
118
|
+
find_args: dict[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]],
|
|
119
|
+
allow_spaces: bool = False
|
|
120
|
+
) -> Args:
|
|
99
121
|
"""Will search for the specified arguments in the command line
|
|
100
122
|
arguments and return the results as a special `Args` object.\n
|
|
101
123
|
----------------------------------------------------------------
|
|
102
|
-
The `find_args` dictionary
|
|
124
|
+
The `find_args` dictionary can have the following structures for each alias:
|
|
125
|
+
1. Simple list/tuple of flags (when no default value is needed):
|
|
126
|
+
```python
|
|
127
|
+
"alias_name": ["-f", "--flag"]
|
|
128
|
+
```
|
|
129
|
+
2. Dictionary with 'flags' and optional 'default':
|
|
130
|
+
```python
|
|
131
|
+
"alias_name": {
|
|
132
|
+
"flags": ["-f", "--flag"],
|
|
133
|
+
"default": "some_value" # Optional
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
Example `find_args`:
|
|
103
137
|
```python
|
|
104
138
|
find_args={
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
139
|
+
"arg1": { # With default
|
|
140
|
+
"flags": ["-a1", "--arg1"],
|
|
141
|
+
"default": "default_val"
|
|
142
|
+
},
|
|
143
|
+
"arg2": ("-a2", "--arg2"), # Without default (original format)
|
|
144
|
+
"arg3": ["-a3"], # Without default (list format)
|
|
145
|
+
"arg4": { # Flag with default True
|
|
146
|
+
"flags": ["-f"],
|
|
147
|
+
"default": True
|
|
148
|
+
}
|
|
108
149
|
}
|
|
109
150
|
```
|
|
110
|
-
|
|
111
|
-
`python script.py -a1 "
|
|
112
|
-
...it would return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
...which can be accessed like this:\n
|
|
121
|
-
- `Args.<arg_alias>.exists` is `True` if any of the specified
|
|
122
|
-
args were found and `False` if not
|
|
123
|
-
- `Args.<arg_alias>.value` the value from behind the found arg,
|
|
124
|
-
`None` if no value was found\n
|
|
151
|
+
If the script is called via the command line:\n
|
|
152
|
+
`python script.py -a1 "value1" --arg2 -f`\n
|
|
153
|
+
...it would return an `Args` object where:
|
|
154
|
+
- `args.arg1.exists` is `True`, `args.arg1.value` is `"value1"`
|
|
155
|
+
- `args.arg2.exists` is `True`, `args.arg2.value` is `True` (flag present without value)
|
|
156
|
+
- `args.arg3.exists` is `False`, `args.arg3.value` is `None` (not present, no default)
|
|
157
|
+
- `args.arg4.exists` is `True`, `args.arg4.value` is `True` (flag present, overrides default)
|
|
158
|
+
- If an arg defined in `find_args` is *not* present in the command line:
|
|
159
|
+
- `exists` will be `False`
|
|
160
|
+
- `value` will be the specified `default` value, or `None` if no default was specified.\n
|
|
125
161
|
----------------------------------------------------------------
|
|
126
162
|
Normally if `allow_spaces` is false, it will take a space as
|
|
127
163
|
the end of an args value. If it is true, it will take spaces as
|
|
@@ -130,20 +166,40 @@ class Console:
|
|
|
130
166
|
args = _sys.argv[1:]
|
|
131
167
|
args_len = len(args)
|
|
132
168
|
arg_lookup = {}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
169
|
+
results = {}
|
|
170
|
+
for alias, config in find_args.items():
|
|
171
|
+
flags = None
|
|
172
|
+
default_value = None
|
|
173
|
+
if isinstance(config, (list, tuple)):
|
|
174
|
+
flags = config
|
|
175
|
+
elif isinstance(config, dict):
|
|
176
|
+
if "flags" not in config:
|
|
177
|
+
raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
|
|
178
|
+
flags = config["flags"]
|
|
179
|
+
default_value = config.get("default")
|
|
180
|
+
if not isinstance(flags, (list, tuple)):
|
|
181
|
+
raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
|
|
182
|
+
else:
|
|
183
|
+
raise TypeError(f"Invalid configuration type for alias '{alias}'. Must be a list, tuple, or dict.")
|
|
184
|
+
results[alias] = {"exists": False, "value": default_value}
|
|
185
|
+
for flag in flags:
|
|
186
|
+
if flag in arg_lookup:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Duplicate flag '{flag}' found. It's assigned to both '{arg_lookup[flag]}' and '{alias}'."
|
|
189
|
+
)
|
|
190
|
+
arg_lookup[flag] = alias
|
|
137
191
|
i = 0
|
|
138
192
|
while i < args_len:
|
|
139
193
|
arg = args[i]
|
|
140
|
-
|
|
141
|
-
if
|
|
142
|
-
results[
|
|
194
|
+
alias = arg_lookup.get(arg)
|
|
195
|
+
if alias:
|
|
196
|
+
results[alias]["exists"] = True
|
|
197
|
+
value_found_after_flag = False
|
|
143
198
|
if i + 1 < args_len and not args[i + 1].startswith("-"):
|
|
144
199
|
if not allow_spaces:
|
|
145
|
-
results[
|
|
200
|
+
results[alias]["value"] = String.to_type(args[i + 1])
|
|
146
201
|
i += 1
|
|
202
|
+
value_found_after_flag = True
|
|
147
203
|
else:
|
|
148
204
|
value_parts = []
|
|
149
205
|
j = i + 1
|
|
@@ -151,8 +207,11 @@ class Console:
|
|
|
151
207
|
value_parts.append(args[j])
|
|
152
208
|
j += 1
|
|
153
209
|
if value_parts:
|
|
154
|
-
results[
|
|
210
|
+
results[alias]["value"] = String.to_type(" ".join(value_parts))
|
|
155
211
|
i = j - 1
|
|
212
|
+
value_found_after_flag = True
|
|
213
|
+
if not value_found_after_flag:
|
|
214
|
+
results[alias]["value"] = True
|
|
156
215
|
i += 1
|
|
157
216
|
return Args(**results)
|
|
158
217
|
|
|
@@ -210,7 +269,7 @@ class Console:
|
|
|
210
269
|
title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
|
|
211
270
|
title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
|
|
212
271
|
if format_linebreaks:
|
|
213
|
-
clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True)
|
|
272
|
+
clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
|
|
214
273
|
prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
|
|
215
274
|
prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
|
|
216
275
|
prompt = f"\n{' ' * title_len}\t".join(Console.__add_back_removed_parts(list(prompt_lst), removals))
|
|
@@ -384,7 +443,7 @@ class Console:
|
|
|
384
443
|
-----------------------------------------------------------------------------------
|
|
385
444
|
The box content can be formatted with special formatting codes. For more detailed
|
|
386
445
|
information about formatting codes, see `xx_format_codes` module documentation."""
|
|
387
|
-
lines = [line.
|
|
446
|
+
lines = [line.rstrip() for val in values for line in val.splitlines()]
|
|
388
447
|
unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
|
|
389
448
|
max_line_len = max(len(line) for line in unfmt_lines)
|
|
390
449
|
pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
|
|
@@ -486,7 +545,7 @@ class Console:
|
|
|
486
545
|
last_console_width = 0
|
|
487
546
|
|
|
488
547
|
def update_display(console_width: int) -> None:
|
|
489
|
-
nonlocal
|
|
548
|
+
nonlocal last_line_count, last_console_width
|
|
490
549
|
lines = String.split_count(str(prompt) + (mask_char * len(result) if mask_char else result), console_width)
|
|
491
550
|
line_count = len(lines)
|
|
492
551
|
if (line_count > 1 or line_count < last_line_count) and not last_line_count == 1:
|
xulbux/xx_data.py
CHANGED
|
@@ -2,13 +2,14 @@ from ._consts_ import COLOR
|
|
|
2
2
|
from .xx_format_codes import FormatCodes
|
|
3
3
|
from .xx_string import String
|
|
4
4
|
|
|
5
|
-
from typing import TypeAlias, Optional, Union
|
|
5
|
+
from typing import TypeAlias, Optional, Union, Any
|
|
6
6
|
import base64 as _base64
|
|
7
7
|
import math as _math
|
|
8
8
|
import re as _re
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
|
|
12
|
+
IndexIterable: TypeAlias = Union[list, tuple, set, frozenset]
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class Data:
|
|
@@ -42,16 +43,23 @@ class Data:
|
|
|
42
43
|
@staticmethod
|
|
43
44
|
def chars_count(data: DataStructure) -> int:
|
|
44
45
|
"""The sum of all the characters amount including the keys in dictionaries."""
|
|
46
|
+
chars_count = 0
|
|
45
47
|
if isinstance(data, dict):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
for k, v in data.items():
|
|
49
|
+
chars_count += len(str(k)) + (Data.chars_count(v) if isinstance(v, DataStructure) else len(str(v)))
|
|
50
|
+
elif isinstance(data, IndexIterable):
|
|
51
|
+
for item in data:
|
|
52
|
+
chars_count += Data.chars_count(item) if isinstance(item, DataStructure) else len(str(item))
|
|
53
|
+
return chars_count
|
|
48
54
|
|
|
49
55
|
@staticmethod
|
|
50
56
|
def strip(data: DataStructure) -> DataStructure:
|
|
51
57
|
"""Removes leading and trailing whitespaces from the data structure's items."""
|
|
52
58
|
if isinstance(data, dict):
|
|
53
|
-
return {k: Data.strip(v) for k, v in data.items()}
|
|
54
|
-
|
|
59
|
+
return {k.strip(): Data.strip(v) if isinstance(v, DataStructure) else v.strip() for k, v in data.items()}
|
|
60
|
+
if isinstance(data, IndexIterable):
|
|
61
|
+
return type(data)(Data.strip(item) if isinstance(item, DataStructure) else item.strip() for item in data)
|
|
62
|
+
return data
|
|
55
63
|
|
|
56
64
|
@staticmethod
|
|
57
65
|
def remove_empty_items(data: DataStructure, spaces_are_empty: bool = False) -> DataStructure:
|
|
@@ -59,18 +67,15 @@ class Data:
|
|
|
59
67
|
If `spaces_are_empty` is true, it will count items with only spaces as empty."""
|
|
60
68
|
if isinstance(data, dict):
|
|
61
69
|
return {
|
|
62
|
-
k: (
|
|
63
|
-
v if not isinstance(v,
|
|
64
|
-
(list, tuple, set, frozenset, dict)) else Data.remove_empty_items(v, spaces_are_empty)
|
|
65
|
-
)
|
|
70
|
+
k: (v if not isinstance(v, DataStructure) else Data.remove_empty_items(v, spaces_are_empty))
|
|
66
71
|
for k, v in data.items() if not String.is_empty(v, spaces_are_empty)
|
|
67
72
|
}
|
|
68
|
-
if isinstance(data,
|
|
73
|
+
if isinstance(data, IndexIterable):
|
|
69
74
|
return type(data)(
|
|
70
|
-
item for item in
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
item for item in
|
|
76
|
+
((item if not isinstance(item, DataStructure) else Data.remove_empty_items(item, spaces_are_empty))
|
|
77
|
+
for item in data if not String.is_empty(item, spaces_are_empty))
|
|
78
|
+
if item not in ([], (), {}, set(), frozenset())
|
|
74
79
|
)
|
|
75
80
|
return data
|
|
76
81
|
|
|
@@ -78,17 +83,25 @@ class Data:
|
|
|
78
83
|
def remove_duplicates(data: DataStructure) -> DataStructure:
|
|
79
84
|
"""Removes all duplicates from the data structure."""
|
|
80
85
|
if isinstance(data, dict):
|
|
81
|
-
return {k: Data.remove_duplicates(v) for k, v in data.items()}
|
|
86
|
+
return {k: Data.remove_duplicates(v) if isinstance(v, DataStructure) else v for k, v in data.items()}
|
|
82
87
|
if isinstance(data, (list, tuple)):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
result = []
|
|
89
|
+
for item in data:
|
|
90
|
+
processed_item = Data.remove_duplicates(item) if isinstance(item, DataStructure) else item
|
|
91
|
+
is_duplicate = False
|
|
92
|
+
for existing_item in result:
|
|
93
|
+
if processed_item == existing_item:
|
|
94
|
+
is_duplicate = True
|
|
95
|
+
break
|
|
96
|
+
if not is_duplicate:
|
|
97
|
+
result.append(processed_item)
|
|
98
|
+
return type(data)(result)
|
|
87
99
|
if isinstance(data, (set, frozenset)):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
processed_elements = set()
|
|
101
|
+
for item in data:
|
|
102
|
+
processed_item = Data.remove_duplicates(item) if isinstance(item, DataStructure) else item
|
|
103
|
+
processed_elements.add(processed_item)
|
|
104
|
+
return type(data)(processed_elements)
|
|
92
105
|
return data
|
|
93
106
|
|
|
94
107
|
@staticmethod
|
|
@@ -161,13 +174,13 @@ class Data:
|
|
|
161
174
|
else:
|
|
162
175
|
return None if s.lstrip().startswith(comment_start) else s.strip() or None
|
|
163
176
|
|
|
164
|
-
def process_item(item:
|
|
177
|
+
def process_item(item: Any) -> Any:
|
|
165
178
|
if isinstance(item, dict):
|
|
166
179
|
return {
|
|
167
180
|
k: v
|
|
168
181
|
for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
|
|
169
182
|
}
|
|
170
|
-
if isinstance(item,
|
|
183
|
+
if isinstance(item, IndexIterable):
|
|
171
184
|
processed = (v for v in map(process_item, item) if v is not None)
|
|
172
185
|
return type(item)(processed)
|
|
173
186
|
if isinstance(item, str):
|
|
@@ -283,7 +296,7 @@ class Data:
|
|
|
283
296
|
if ignore_not_found:
|
|
284
297
|
return None
|
|
285
298
|
raise KeyError(f"Key '{key}' not found in dict.")
|
|
286
|
-
elif isinstance(data_obj,
|
|
299
|
+
elif isinstance(data_obj, IndexIterable):
|
|
287
300
|
try:
|
|
288
301
|
idx = int(key)
|
|
289
302
|
data_obj = list(data_obj)[idx] # CONVERT TO LIST FOR INDEXING
|
|
@@ -310,7 +323,7 @@ class Data:
|
|
|
310
323
|
return results if len(results) > 1 else results[0] if results else None
|
|
311
324
|
|
|
312
325
|
@staticmethod
|
|
313
|
-
def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) ->
|
|
326
|
+
def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) -> Any:
|
|
314
327
|
"""Retrieves the value from `data` using the provided `path_id`.\n
|
|
315
328
|
-------------------------------------------------------------------------------------------------
|
|
316
329
|
Input your `data` along with a `path_id` that was created before using `Data.get_path_id()`.
|
|
@@ -319,7 +332,7 @@ class Data:
|
|
|
319
332
|
The function will return the value (or key) from the path ID location, as long as the structure
|
|
320
333
|
of `data` hasn't changed since creating the path ID to that value."""
|
|
321
334
|
|
|
322
|
-
def get_nested(data: DataStructure, path: list[int], get_key: bool) ->
|
|
335
|
+
def get_nested(data: DataStructure, path: list[int], get_key: bool) -> Any:
|
|
323
336
|
parent = None
|
|
324
337
|
for i, idx in enumerate(path):
|
|
325
338
|
if isinstance(data, dict):
|
|
@@ -328,7 +341,7 @@ class Data:
|
|
|
328
341
|
return keys[idx]
|
|
329
342
|
parent = data
|
|
330
343
|
data = data[keys[idx]]
|
|
331
|
-
elif isinstance(data,
|
|
344
|
+
elif isinstance(data, IndexIterable):
|
|
332
345
|
if i == len(path) - 1 and get_key:
|
|
333
346
|
if parent is None or not isinstance(parent, dict):
|
|
334
347
|
raise ValueError("Cannot get key from a non-dict parent")
|
|
@@ -342,45 +355,39 @@ class Data:
|
|
|
342
355
|
return get_nested(data, Data.__sep_path_id(path_id), get_key)
|
|
343
356
|
|
|
344
357
|
@staticmethod
|
|
345
|
-
def set_value_by_path_id(data: DataStructure, update_values:
|
|
358
|
+
def set_value_by_path_id(data: DataStructure, update_values: dict[str, Any]) -> list | tuple | dict:
|
|
346
359
|
"""Updates the value/s from `update_values` in the `data`.\n
|
|
347
360
|
--------------------------------------------------------------------------------
|
|
348
361
|
Input a list, tuple or dict as `data`, along with `update_values`, which is a
|
|
349
|
-
path
|
|
350
|
-
|
|
351
|
-
|
|
362
|
+
dictionary where keys are path IDs and values are the new values to insert:
|
|
363
|
+
{ "1>": "new value", "path_id2": ["new value 1", "new value 2"], ... }
|
|
364
|
+
The path IDs should have been created using `Data.get_path_id()`.\n
|
|
352
365
|
--------------------------------------------------------------------------------
|
|
353
366
|
The value from path ID will be changed to the new value, as long as the
|
|
354
367
|
structure of `data` hasn't changed since creating the path ID to that value."""
|
|
355
368
|
|
|
356
|
-
def update_nested(data: DataStructure, path: list[int], value:
|
|
369
|
+
def update_nested(data: DataStructure, path: list[int], value: Any) -> DataStructure:
|
|
357
370
|
if len(path) == 1:
|
|
358
371
|
if isinstance(data, dict):
|
|
359
|
-
keys = list(data.keys())
|
|
360
|
-
data = dict(data)
|
|
372
|
+
keys, data = list(data.keys()), dict(data)
|
|
361
373
|
data[keys[path[0]]] = value
|
|
362
|
-
elif isinstance(data,
|
|
363
|
-
data = list(data)
|
|
374
|
+
elif isinstance(data, IndexIterable):
|
|
375
|
+
was_t, data = type(data), list(data)
|
|
364
376
|
data[path[0]] = value
|
|
365
|
-
data =
|
|
377
|
+
data = was_t(data)
|
|
366
378
|
else:
|
|
367
379
|
if isinstance(data, dict):
|
|
368
|
-
keys = list(data.keys())
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
data
|
|
372
|
-
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
373
|
-
data = list(data)
|
|
380
|
+
keys, data = list(data.keys()), dict(data)
|
|
381
|
+
data[keys[path[0]]] = update_nested(data[keys[path[0]]], path[1:], value)
|
|
382
|
+
elif isinstance(data, IndexIterable):
|
|
383
|
+
was_t, data = type(data), list(data)
|
|
374
384
|
data[path[0]] = update_nested(data[path[0]], path[1:], value)
|
|
375
|
-
data =
|
|
385
|
+
data = was_t(data)
|
|
376
386
|
return data
|
|
377
387
|
|
|
378
|
-
|
|
379
|
-
update_values = [update_values]
|
|
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]
|
|
388
|
+
valid_entries = [(path_id, new_val) for path_id, new_val in update_values.items()]
|
|
382
389
|
if not valid_entries:
|
|
383
|
-
raise ValueError(f"No valid update_values found: {update_values}")
|
|
390
|
+
raise ValueError(f"No valid update_values found in dictionary: {update_values}")
|
|
384
391
|
for path_id, new_val in valid_entries:
|
|
385
392
|
path = Data.__sep_path_id(path_id)
|
|
386
393
|
data = update_nested(data, path, new_val)
|
|
@@ -431,12 +438,12 @@ class Data:
|
|
|
431
438
|
for k, v in punct_map.items()
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
def format_value(value:
|
|
441
|
+
def format_value(value: Any, current_indent: int = None) -> str:
|
|
435
442
|
if current_indent is not None and isinstance(value, dict):
|
|
436
443
|
return format_dict(value, current_indent + indent)
|
|
437
444
|
elif current_indent is not None and hasattr(value, "__dict__"):
|
|
438
445
|
return format_dict(value.__dict__, current_indent + indent)
|
|
439
|
-
elif current_indent is not None and isinstance(value,
|
|
446
|
+
elif current_indent is not None and isinstance(value, IndexIterable):
|
|
440
447
|
return format_sequence(value, current_indent + indent)
|
|
441
448
|
elif isinstance(value, (bytes, bytearray)):
|
|
442
449
|
obj_dict = Data.serialize_bytes(value)
|
xulbux/xx_env_path.py
CHANGED
xulbux/xx_file.py
CHANGED
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
from .xx_string import String
|
|
2
|
-
from .xx_path import Path
|
|
3
2
|
|
|
4
3
|
import os as _os
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class SameContentFileExistsError(FileExistsError):
|
|
8
|
-
|
|
7
|
+
...
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class File:
|
|
12
11
|
|
|
13
12
|
@staticmethod
|
|
14
|
-
def rename_extension(
|
|
13
|
+
def rename_extension(
|
|
14
|
+
file: str,
|
|
15
|
+
new_extension: str,
|
|
16
|
+
full_extension: bool = False,
|
|
17
|
+
camel_case_filename: bool = False,
|
|
18
|
+
) -> str:
|
|
15
19
|
"""Rename the extension of a file.\n
|
|
16
20
|
--------------------------------------------------------------------------
|
|
21
|
+
If `full_extension` is true, everything after the first dot in the
|
|
22
|
+
filename will be treated as the extension to replace. Otherwise, only the
|
|
23
|
+
part after the last dot is replaced.\n
|
|
17
24
|
If the `camel_case_filename` parameter is true, the filename will be made
|
|
18
25
|
CamelCase in addition to changing the files extension."""
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
normalized_file = _os.path.normpath(file)
|
|
27
|
+
directory, filename_with_ext = _os.path.split(normalized_file)
|
|
28
|
+
if full_extension:
|
|
29
|
+
try:
|
|
30
|
+
first_dot_index = filename_with_ext.index('.')
|
|
31
|
+
filename = filename_with_ext[:first_dot_index]
|
|
32
|
+
except ValueError:
|
|
33
|
+
filename = filename_with_ext
|
|
34
|
+
else:
|
|
35
|
+
filename, _ = _os.path.splitext(filename_with_ext)
|
|
21
36
|
if camel_case_filename:
|
|
22
37
|
filename = String.to_camel_case(filename)
|
|
38
|
+
if new_extension and not new_extension.startswith('.'):
|
|
39
|
+
new_extension = '.' + new_extension
|
|
23
40
|
return _os.path.join(directory, f"{filename}{new_extension}")
|
|
24
41
|
|
|
25
42
|
@staticmethod
|
|
@@ -40,24 +57,3 @@ class File:
|
|
|
40
57
|
f.write(content)
|
|
41
58
|
full_path = _os.path.abspath(file)
|
|
42
59
|
return full_path
|
|
43
|
-
|
|
44
|
-
@staticmethod
|
|
45
|
-
def extend_or_make_path(
|
|
46
|
-
file: str,
|
|
47
|
-
search_in: str | list[str] = None,
|
|
48
|
-
prefer_base_dir: bool = True,
|
|
49
|
-
correct_paths: bool = False,
|
|
50
|
-
) -> str:
|
|
51
|
-
"""Tries to find the file and extend the path to be absolute and if the file was not found:\n
|
|
52
|
-
Generate the absolute path to the file in the CWD or the running program's base-directory.\n
|
|
53
|
-
----------------------------------------------------------------------------------------------
|
|
54
|
-
If the `file` is not found in predefined directories, it will be searched in the `search_in`
|
|
55
|
-
directory/directories. If the file is still not found, it will return the path to the file in
|
|
56
|
-
the base-dir per default or to the file in the CWD if `prefer_base_dir` is set to `False`.\n
|
|
57
|
-
----------------------------------------------------------------------------------------------
|
|
58
|
-
If `correct_paths` is true, it is possible to have typos in the `search_in` path/s and it
|
|
59
|
-
will still find the file if it is under one of those paths."""
|
|
60
|
-
try:
|
|
61
|
-
return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
|
|
62
|
-
except FileNotFoundError:
|
|
63
|
-
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
|
@@ -333,11 +333,16 @@ class FormatCodes:
|
|
|
333
333
|
return ansi_string.replace(ANSI.char, ANSI.escaped_char)
|
|
334
334
|
|
|
335
335
|
@staticmethod
|
|
336
|
-
def remove_ansi(
|
|
336
|
+
def remove_ansi(
|
|
337
|
+
ansi_string: str,
|
|
338
|
+
get_removals: bool = False,
|
|
339
|
+
_ignore_linebreaks: bool = False,
|
|
340
|
+
) -> str | tuple[str, tuple[tuple[int, str], ...]]:
|
|
337
341
|
"""Removes all ANSI codes from the string.\n
|
|
338
342
|
--------------------------------------------------------------------------------------------------
|
|
339
343
|
If `get_removals` is true, additionally to the cleaned string, a list of tuples will be returned.
|
|
340
|
-
Each tuple contains the position of the removed ansi code and the removed ansi code
|
|
344
|
+
Each tuple contains the position of the removed ansi code and the removed ansi code.\n
|
|
345
|
+
If `_ignore_linebreaks` is true, linebreaks will be ignored for the removal positions."""
|
|
341
346
|
if get_removals:
|
|
342
347
|
removals = []
|
|
343
348
|
|
|
@@ -348,18 +353,30 @@ class FormatCodes:
|
|
|
348
353
|
removals.append((start_pos, match.group()))
|
|
349
354
|
return ""
|
|
350
355
|
|
|
351
|
-
clean_string = _COMPILED["ansi_seq"].sub(
|
|
352
|
-
|
|
356
|
+
clean_string = _COMPILED["ansi_seq"].sub(
|
|
357
|
+
replacement,
|
|
358
|
+
ansi_string.replace("\n", "") if _ignore_linebreaks else ansi_string
|
|
359
|
+
)
|
|
360
|
+
return _COMPILED["ansi_seq"].sub("", ansi_string) if _ignore_linebreaks else clean_string, tuple(removals)
|
|
353
361
|
else:
|
|
354
362
|
return _COMPILED["ansi_seq"].sub("", ansi_string)
|
|
355
363
|
|
|
356
364
|
@staticmethod
|
|
357
|
-
def remove_formatting(
|
|
365
|
+
def remove_formatting(
|
|
366
|
+
string: str,
|
|
367
|
+
get_removals: bool = False,
|
|
368
|
+
_ignore_linebreaks: bool = False,
|
|
369
|
+
) -> str | tuple[str, tuple[tuple[int, str], ...]]:
|
|
358
370
|
"""Removes all formatting codes from the string.\n
|
|
359
|
-
|
|
371
|
+
---------------------------------------------------------------------------------------------------
|
|
360
372
|
If `get_removals` is true, additionally to the cleaned string, a list of tuples will be returned.
|
|
361
|
-
Each tuple contains the position of the removed formatting code and the removed formatting code
|
|
362
|
-
|
|
373
|
+
Each tuple contains the position of the removed formatting code and the removed formatting code.\n
|
|
374
|
+
If `_ignore_linebreaks` is true, linebreaks will be ignored for the removal positions."""
|
|
375
|
+
return FormatCodes.remove_ansi(
|
|
376
|
+
FormatCodes.to_ansi(string),
|
|
377
|
+
get_removals=get_removals,
|
|
378
|
+
_ignore_linebreaks=_ignore_linebreaks,
|
|
379
|
+
)
|
|
363
380
|
|
|
364
381
|
@staticmethod
|
|
365
382
|
def __config_console() -> None:
|