xulbux 1.5.5__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.
- xulbux/__help__.py +74 -0
- xulbux/__init__.py +47 -0
- xulbux/_consts_.py +147 -0
- xulbux/xx_cmd.py +240 -0
- xulbux/xx_code.py +102 -0
- xulbux/xx_color.py +799 -0
- xulbux/xx_data.py +431 -0
- xulbux/xx_env_vars.py +60 -0
- xulbux/xx_file.py +50 -0
- xulbux/xx_format_codes.py +212 -0
- xulbux/xx_json.py +81 -0
- xulbux/xx_path.py +97 -0
- xulbux/xx_regex.py +124 -0
- xulbux/xx_string.py +116 -0
- xulbux/xx_system.py +75 -0
- xulbux-1.5.5.dist-info/METADATA +97 -0
- xulbux-1.5.5.dist-info/RECORD +20 -0
- xulbux-1.5.5.dist-info/WHEEL +4 -0
- xulbux-1.5.5.dist-info/entry_points.txt +2 -0
- xulbux-1.5.5.dist-info/licenses/LICENSE +21 -0
xulbux/xx_data.py
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import math as _math
|
|
2
|
+
import re as _re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Data:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def chars_count(data:list|tuple|set|frozenset|dict) -> int:
|
|
11
|
+
"""The sum of all the characters including the keys in dictionaries."""
|
|
12
|
+
if isinstance(data, dict):
|
|
13
|
+
return sum(len(str(k)) + len(str(v)) for k, v in data.items())
|
|
14
|
+
return sum(len(str(item)) for item in data)
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def strip(data:list|tuple|dict) -> list|tuple|dict:
|
|
18
|
+
if isinstance(data, dict):
|
|
19
|
+
return {k: v.strip() if isinstance(v, str) else Data.strip(v) for k, v in data.items()}
|
|
20
|
+
elif isinstance(data, (list, tuple)):
|
|
21
|
+
stripped = [item.strip() if isinstance(item, str) else Data.strip(item) for item in data]
|
|
22
|
+
return tuple(stripped) if isinstance(data, tuple) else stripped
|
|
23
|
+
return data.strip() if isinstance(data, str) else data
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def remove(data:list|tuple|dict, items:list[str]) -> list|tuple|dict:
|
|
27
|
+
"""Remove multiple items from lists and tuples or keys from dictionaries."""
|
|
28
|
+
if isinstance(data, (list, tuple)):
|
|
29
|
+
result = [k for k in data if k not in items]
|
|
30
|
+
return result if isinstance(data, list) else tuple(result)
|
|
31
|
+
elif isinstance(data, dict):
|
|
32
|
+
return {k: v for k, v in data.items() if k not in items}
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def remove_empty_items(data:list|tuple|dict, spaces_are_empty:bool = False) -> list|tuple|dict:
|
|
36
|
+
if isinstance(data, dict):
|
|
37
|
+
filtered_dict = {}
|
|
38
|
+
for key, value in data.items():
|
|
39
|
+
if isinstance(value, (list, tuple, dict)):
|
|
40
|
+
filtered_value = Data.remove_empty_items(value, spaces_are_empty)
|
|
41
|
+
if filtered_value: filtered_dict[key] = filtered_value
|
|
42
|
+
elif value not in (None, '') and not ((spaces_are_empty and isinstance(value, str)) and value.strip() in (None, '')):
|
|
43
|
+
filtered_dict[key] = value
|
|
44
|
+
return filtered_dict
|
|
45
|
+
filtered = []
|
|
46
|
+
for item in data:
|
|
47
|
+
if isinstance(item, (list, tuple, dict)):
|
|
48
|
+
deduped_item = Data.remove_empty_items(item, spaces_are_empty)
|
|
49
|
+
if deduped_item:
|
|
50
|
+
if isinstance(item, tuple):
|
|
51
|
+
deduped_item = tuple(deduped_item)
|
|
52
|
+
filtered.append(deduped_item)
|
|
53
|
+
elif item not in (None, '') and not ((spaces_are_empty and isinstance(item, str)) and item.strip() in (None, '')):
|
|
54
|
+
filtered.append(item)
|
|
55
|
+
return tuple(filtered) if isinstance(data, tuple) else filtered
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def remove_duplicates(data:list|tuple|dict) -> list|tuple|dict:
|
|
59
|
+
if isinstance(data, dict):
|
|
60
|
+
return {k: Data.remove_duplicates(v) for k, v in data.items()}
|
|
61
|
+
elif isinstance(data, (list, tuple)):
|
|
62
|
+
unique_items = []
|
|
63
|
+
for item in data:
|
|
64
|
+
if isinstance(item, (list, tuple, set, dict)):
|
|
65
|
+
deduped_item = Data.remove_duplicates(item)
|
|
66
|
+
if deduped_item not in unique_items:
|
|
67
|
+
unique_items.append(deduped_item)
|
|
68
|
+
elif item not in unique_items:
|
|
69
|
+
unique_items.append(item)
|
|
70
|
+
return tuple(unique_items) if isinstance(data, tuple) else unique_items
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def remove_comments(data:list|tuple|dict, comment_start:str = '>>', comment_end:str = '<<', comment_sep:str = '') -> list|tuple|dict:
|
|
75
|
+
"""Remove comments from a list, tuple or dictionary.\n
|
|
76
|
+
-----------------------------------------------------------------------------------------------------------------
|
|
77
|
+
The `data` parameter is your list, tuple or dictionary, where the comments should get removed from.<br>
|
|
78
|
+
The `comment_start` parameter is the string that marks the start of a comment inside `data`. (default: `>>`)<br>
|
|
79
|
+
The `comment_end` parameter is the string that marks the end of a comment inside `data`. (default: `<<`)<br>
|
|
80
|
+
The `comment_sep` parameter is a string with which a comment will be replaced, if it is between strings.\n
|
|
81
|
+
-----------------------------------------------------------------------------------------------------------------
|
|
82
|
+
Examples:\n
|
|
83
|
+
```python\n data = {
|
|
84
|
+
'key1': [
|
|
85
|
+
'>> COMMENT IN THE BEGINNING OF THE STRING << value1',
|
|
86
|
+
'value2 >> COMMENT IN THE END OF THE STRING',
|
|
87
|
+
'val>> COMMENT IN THE MIDDLE OF THE STRING <<ue3',
|
|
88
|
+
'>> FULL VALUE IS A COMMENT value4'
|
|
89
|
+
],
|
|
90
|
+
'>> FULL KEY + ALL ITS VALUES ARE A COMMENT key2': [
|
|
91
|
+
'value',
|
|
92
|
+
'value',
|
|
93
|
+
'value'
|
|
94
|
+
],
|
|
95
|
+
'key3': '>> ALL THE KEYS VALUES ARE COMMENTS value'
|
|
96
|
+
}
|
|
97
|
+
processed_data = Data.remove_comments(data, comment_start='>>', comment_end='<<', comment_sep='__')\n```
|
|
98
|
+
-----------------------------------------------------------------------------------------------------------------
|
|
99
|
+
For this example, `processed_data` will be:
|
|
100
|
+
```python\n {
|
|
101
|
+
'key1': [
|
|
102
|
+
'value1',
|
|
103
|
+
'value2',
|
|
104
|
+
'val__ue3'
|
|
105
|
+
],
|
|
106
|
+
'key3': None
|
|
107
|
+
}\n```
|
|
108
|
+
For `key1`, all the comments will just be removed, except at `value3` and `value4`:<br>
|
|
109
|
+
`value3` The comment is removed and the parts left and right are joined through `comment_sep`.<br>
|
|
110
|
+
`value4` The whole value is removed, since the whole value was a comment.<br>
|
|
111
|
+
For `key2`, the key, including its whole values will be removed.<br>
|
|
112
|
+
For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`."""
|
|
113
|
+
def process_item(item:dict|list|tuple|str) -> dict|list|tuple|str|None:
|
|
114
|
+
if isinstance(item, dict):
|
|
115
|
+
processed_dict = {}
|
|
116
|
+
for key, val in item.items():
|
|
117
|
+
processed_key = process_item(key)
|
|
118
|
+
if processed_key is not None:
|
|
119
|
+
processed_val = process_item(val)
|
|
120
|
+
if isinstance(val, (list, tuple, dict)):
|
|
121
|
+
if processed_val: processed_dict[processed_key] = processed_val
|
|
122
|
+
elif processed_val is not None:
|
|
123
|
+
processed_dict[processed_key] = processed_val
|
|
124
|
+
else:
|
|
125
|
+
processed_dict[processed_key] = None
|
|
126
|
+
return processed_dict
|
|
127
|
+
elif isinstance(item, list):
|
|
128
|
+
return [v for v in (process_item(val) for val in item) if v is not None]
|
|
129
|
+
elif isinstance(item, tuple):
|
|
130
|
+
return tuple(v for v in (process_item(val) for val in item) if v is not None)
|
|
131
|
+
elif isinstance(item, str):
|
|
132
|
+
if comment_end:
|
|
133
|
+
no_comments = _re.sub(rf'^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$',
|
|
134
|
+
lambda m: f'{m.group(1).strip()}{comment_sep if (m.group(1).strip() not in ["", None]) and (m.group(2).strip() not in ["", None]) else ""}{m.group(2).strip()}', item)
|
|
135
|
+
else:
|
|
136
|
+
no_comments = None if item.lstrip().startswith(comment_start) else item
|
|
137
|
+
return no_comments.strip() if no_comments and no_comments.strip() != '' else None
|
|
138
|
+
else:
|
|
139
|
+
return item
|
|
140
|
+
return process_item(data)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def is_equal(data1:list|tuple|dict, data2:list|tuple|dict, ignore_paths:str|list[str] = '', comment_start:str = '>>', comment_end:str = '<<', sep:str = '->') -> bool:
|
|
144
|
+
"""Compares two structures and returns `True` if they are equal and `False` otherwise.\n
|
|
145
|
+
⇾ **Will not detect, if a key-name has changed, only if removed or added.**\n
|
|
146
|
+
------------------------------------------------------------------------------------------------
|
|
147
|
+
Ignores the specified (found) key/s or item/s from `ignore_paths`. Comments are not ignored<br>
|
|
148
|
+
when comparing. `comment_start` and `comment_end` are only used for key recognition.\n
|
|
149
|
+
------------------------------------------------------------------------------------------------
|
|
150
|
+
The paths from `ignore_paths` work exactly the same way as the paths from `value_paths`<br>
|
|
151
|
+
in the function `Data.get_path_id()`, just like the `sep` parameter. For more detailed<br>
|
|
152
|
+
explanation, see the documentation of the function `Data.get_path_id()`."""
|
|
153
|
+
def process_ignore_paths(ignore_paths:str|list[str]) -> list[list[str]]:
|
|
154
|
+
if isinstance(ignore_paths, str):
|
|
155
|
+
ignore_paths = [ignore_paths]
|
|
156
|
+
return [path.split(sep) for path in ignore_paths if path]
|
|
157
|
+
def compare(d1:dict|list|tuple, d2:dict|list|tuple, ignore_paths:list[list[str]], current_path:list = []) -> bool:
|
|
158
|
+
if ignore_paths and any(current_path == path[:len(current_path)] and len(current_path) == len(path) for path in ignore_paths):
|
|
159
|
+
return True
|
|
160
|
+
if isinstance(d1, dict) and isinstance(d2, dict):
|
|
161
|
+
if set(d1.keys()) != set(d2.keys()):
|
|
162
|
+
return False
|
|
163
|
+
return all(compare(d1[key], d2[key], ignore_paths, current_path + [key]) for key in d1)
|
|
164
|
+
elif isinstance(d1, (list, tuple)) and isinstance(d2, (list, tuple)):
|
|
165
|
+
if len(d1) != len(d2):
|
|
166
|
+
return False
|
|
167
|
+
return all(compare(item1, item2, ignore_paths, current_path + [str(i)]) for i, (item1, item2) in enumerate(zip(d1, d2)))
|
|
168
|
+
else:
|
|
169
|
+
return d1 == d2
|
|
170
|
+
return compare(Data.remove_comments(data1, comment_start, comment_end), Data.remove_comments(data2, comment_start, comment_end), process_ignore_paths(ignore_paths))
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def get_fingerprint(data:list|tuple|dict) -> list|tuple|dict|None:
|
|
174
|
+
if isinstance(data, dict):
|
|
175
|
+
return {i: type(v).__name__ for i, v in enumerate(data.values())}
|
|
176
|
+
elif isinstance(data, (list, tuple)):
|
|
177
|
+
return {i: type(v).__name__ for i, v in enumerate(data)}
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def get_path_id(data:list|tuple|dict, value_paths:str|list[str], sep:str = '->', ignore_not_found:bool = False) -> str|list[str]:
|
|
182
|
+
"""Generates a unique ID based on the path to a specific value within a nested data structure.\n
|
|
183
|
+
-------------------------------------------------------------------------------------------------
|
|
184
|
+
The `data` parameter is the list, tuple, or dictionary, which the id should be generated for.\n
|
|
185
|
+
-------------------------------------------------------------------------------------------------
|
|
186
|
+
The param `value_path` is a sort of path (or a list of paths) to the value/s to be updated.<br>
|
|
187
|
+
In this example:
|
|
188
|
+
```\n {
|
|
189
|
+
'healthy': {
|
|
190
|
+
'fruit': ['apples', 'bananas', 'oranges'],
|
|
191
|
+
'vegetables': ['carrots', 'broccoli', 'celery']
|
|
192
|
+
}
|
|
193
|
+
}\n```
|
|
194
|
+
... if you want to change the value of `'apples'` to `'strawberries'`, `value_path`<br>
|
|
195
|
+
would be `healthy->fruit->apples` or if you don't know that the value is `apples`<br>
|
|
196
|
+
you can also use the position of the value, so `healthy->fruit->0`.\n
|
|
197
|
+
-------------------------------------------------------------------------------------------------
|
|
198
|
+
The `sep` param is the separator between the keys in the path<br>
|
|
199
|
+
(default is `->` just like in the example above).\n
|
|
200
|
+
-------------------------------------------------------------------------------------------------
|
|
201
|
+
If `ignore_not_found` is `True`, the function will return `None` if the value is not<br>
|
|
202
|
+
found instead of raising an error."""
|
|
203
|
+
if isinstance(value_paths, str): value_paths = [value_paths]
|
|
204
|
+
path_ids = []
|
|
205
|
+
for path in value_paths:
|
|
206
|
+
keys = [k.strip() for k in path.split(str(sep).strip()) if k.strip() != '']
|
|
207
|
+
id_part_len, _path_ids, _obj = 0, [], data
|
|
208
|
+
try:
|
|
209
|
+
for k in keys:
|
|
210
|
+
if isinstance(_obj, dict):
|
|
211
|
+
if k.isdigit():
|
|
212
|
+
raise TypeError(f'Key \'{k}\' is invalid for a dict type.')
|
|
213
|
+
try:
|
|
214
|
+
idx = list(_obj.keys()).index(k)
|
|
215
|
+
_path_ids.append(idx)
|
|
216
|
+
_obj = _obj[k]
|
|
217
|
+
except KeyError:
|
|
218
|
+
if ignore_not_found:
|
|
219
|
+
_path_ids = None
|
|
220
|
+
break
|
|
221
|
+
raise KeyError(f'Key \'{k}\' not found in dict.')
|
|
222
|
+
elif isinstance(_obj, (list, tuple)):
|
|
223
|
+
try:
|
|
224
|
+
idx = int(k)
|
|
225
|
+
_path_ids.append(idx)
|
|
226
|
+
_obj = _obj[idx]
|
|
227
|
+
except ValueError:
|
|
228
|
+
try:
|
|
229
|
+
idx = _obj.index(k)
|
|
230
|
+
_path_ids.append(idx)
|
|
231
|
+
_obj = _obj[idx]
|
|
232
|
+
except ValueError:
|
|
233
|
+
if ignore_not_found:
|
|
234
|
+
_path_ids = None
|
|
235
|
+
break
|
|
236
|
+
raise ValueError(f'Value \'{k}\' not found in list/tuple.')
|
|
237
|
+
else:
|
|
238
|
+
break
|
|
239
|
+
if _path_ids:
|
|
240
|
+
id_part_len = max(id_part_len, len(str(_path_ids[-1])))
|
|
241
|
+
if _path_ids is not None:
|
|
242
|
+
path_ids.append(f'{id_part_len}>{"".join([str(id).zfill(id_part_len) for id in _path_ids])}')
|
|
243
|
+
elif ignore_not_found:
|
|
244
|
+
path_ids.append(None)
|
|
245
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
246
|
+
if ignore_not_found:
|
|
247
|
+
path_ids.append(None)
|
|
248
|
+
else: raise e
|
|
249
|
+
return path_ids if len(path_ids) > 1 else path_ids[0] if len(path_ids) == 1 else None
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def get_value_by_path_id(data:list|tuple|dict, path_id:str, get_key:bool = False) -> any:
|
|
253
|
+
"""Retrieves the value from `data` using the provided `path_id`.\n
|
|
254
|
+
------------------------------------------------------------------------------------
|
|
255
|
+
Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
|
|
256
|
+
that was created before using `Object.get_path_id()`. If `get_key` is True<br>
|
|
257
|
+
and the final item is in a dict, it returns the key instead of the value.\n
|
|
258
|
+
------------------------------------------------------------------------------------
|
|
259
|
+
The function will return the value (or key) from the path-id location, as long as<br>
|
|
260
|
+
the structure of `data` hasn't changed since creating the path-id to that value."""
|
|
261
|
+
def get_nested(data:list|tuple|dict, path:list[int], get_key:bool) -> any:
|
|
262
|
+
parent = None
|
|
263
|
+
for i, idx in enumerate(path):
|
|
264
|
+
if isinstance(data, dict):
|
|
265
|
+
keys = list(data.keys())
|
|
266
|
+
if i == len(path) - 1 and get_key:
|
|
267
|
+
return keys[idx]
|
|
268
|
+
parent = data
|
|
269
|
+
data = data[keys[idx]]
|
|
270
|
+
elif isinstance(data, (list, tuple)):
|
|
271
|
+
if i == len(path) - 1 and get_key:
|
|
272
|
+
if parent is None or not isinstance(parent, dict):
|
|
273
|
+
raise ValueError('Cannot get key from list or tuple without a parent dictionary')
|
|
274
|
+
return next(key for key, value in parent.items() if value is data)
|
|
275
|
+
parent = data
|
|
276
|
+
data = data[idx]
|
|
277
|
+
else:
|
|
278
|
+
raise TypeError(f'Unsupported type {type(data)} at path {path[:i+1]}')
|
|
279
|
+
return data
|
|
280
|
+
path = Data._sep_path_id(path_id)
|
|
281
|
+
return get_nested(data, path, get_key)
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def set_value_by_path_id(data:list|tuple|dict, update_values:str|list[str], sep:str = '::') -> list|tuple|dict:
|
|
285
|
+
"""Updates the value/s from `update_values` in the `data`.\n
|
|
286
|
+
--------------------------------------------------------------------------------
|
|
287
|
+
Input a list, tuple or dict as `data`, along with `update_values`, which is<br>
|
|
288
|
+
a path-id that was created before using `Object.get_path_id()`, together<br>
|
|
289
|
+
with the new value to be inserted where the path-id points to. The path-id<br>
|
|
290
|
+
and the new value are separated by `sep`, which per default is `::`.\n
|
|
291
|
+
--------------------------------------------------------------------------------
|
|
292
|
+
The value from path-id will be changed to the new value, as long as the<br>
|
|
293
|
+
structure of `data` hasn't changed since creating the path-id to that value."""
|
|
294
|
+
def update_nested(data:list|tuple|dict, path:list[int], value:any) -> list|tuple|dict:
|
|
295
|
+
if len(path) == 1:
|
|
296
|
+
if isinstance(data, dict):
|
|
297
|
+
keys = list(data.keys())
|
|
298
|
+
data[keys[path[0]]] = value
|
|
299
|
+
elif isinstance(data, (list, tuple)):
|
|
300
|
+
data = list(data)
|
|
301
|
+
data[path[0]] = value
|
|
302
|
+
data = type(data)(data)
|
|
303
|
+
elif isinstance(data, dict):
|
|
304
|
+
keys = list(data.keys())
|
|
305
|
+
key = keys[path[0]]
|
|
306
|
+
data[key] = update_nested(data[key], path[1:], value)
|
|
307
|
+
elif isinstance(data, (list, tuple)):
|
|
308
|
+
data = list(data)
|
|
309
|
+
data[path[0]] = update_nested(data[path[0]], path[1:], value)
|
|
310
|
+
data = type(data)(data)
|
|
311
|
+
return data
|
|
312
|
+
if isinstance(update_values, str):
|
|
313
|
+
update_values = [update_values]
|
|
314
|
+
valid_entries = [(parts[0].strip(), parts[1]) for update_value in update_values if len(parts := update_value.split(str(sep).strip())) == 2]
|
|
315
|
+
if not valid_entries:
|
|
316
|
+
raise ValueError(f'No valid update_values found: {update_values}')
|
|
317
|
+
path, new_values = (zip(*valid_entries) if valid_entries else ([], []))
|
|
318
|
+
for path_id, new_val in zip(path, new_values):
|
|
319
|
+
path = Data._sep_path_id(path_id)
|
|
320
|
+
data = update_nested(data, path, new_val)
|
|
321
|
+
return data
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def print(data:list|tuple|dict, indent:int = 2, compactness:int = 1, sep:str = ', ', max_width:int = 140, as_json:bool = False, end:str = '\n') -> None:
|
|
325
|
+
"""Print nicely formatted data structures.\n
|
|
326
|
+
------------------------------------------------------------------------------------
|
|
327
|
+
The indentation spaces-amount can be set with with `indent`.<br>
|
|
328
|
+
There are three different levels of `compactness`:<br>
|
|
329
|
+
`0` expands everything possible<br>
|
|
330
|
+
`1` only expands if there's other lists, tuples or dicts inside of data or,<br>
|
|
331
|
+
⠀if the data's content is longer than `max_width`<br>
|
|
332
|
+
`2` keeps everything collapsed (all on one line)\n
|
|
333
|
+
------------------------------------------------------------------------------------
|
|
334
|
+
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
335
|
+
print(Data.to_str(data, indent, compactness, sep, max_width, as_json), end=end, flush=True)
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def to_str(data:list|tuple|dict, indent:int = 2, compactness:int = 1, sep:str = ', ', max_width:int = 140, as_json:bool = False) -> str:
|
|
339
|
+
"""Get nicely formatted data structure-strings.\n
|
|
340
|
+
------------------------------------------------------------------------------------
|
|
341
|
+
The indentation spaces-amount can be set with with `indent`.<br>
|
|
342
|
+
There are three different levels of `compactness`:<br>
|
|
343
|
+
`0` expands everything possible<br>
|
|
344
|
+
`1` only expands if there's other lists, tuples or dicts inside of data or,<br>
|
|
345
|
+
⠀if the data's content is longer than `max_width`<br>
|
|
346
|
+
`2` keeps everything collapsed (all on one line)\n
|
|
347
|
+
------------------------------------------------------------------------------------
|
|
348
|
+
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
349
|
+
def escape_string(s:str, str_quotes:str = '"') -> str:
|
|
350
|
+
s = s.replace('\\', r'\\').replace('\n', r'\n').replace('\r', r'\r').replace('\t', r'\t').replace('\b', r'\b').replace('\f', r'\f').replace('\a', r'\a')
|
|
351
|
+
if str_quotes == '"': s = s.replace(r"\\'", "'").replace(r'"', r'\"')
|
|
352
|
+
elif str_quotes == "'": s = s.replace(r'\\"', '"').replace(r"'", r"\'")
|
|
353
|
+
return s
|
|
354
|
+
def format_value(value:any, current_indent:int) -> str:
|
|
355
|
+
if isinstance(value, dict):
|
|
356
|
+
return format_dict(value, current_indent + indent)
|
|
357
|
+
elif hasattr(value, '__dict__'):
|
|
358
|
+
return format_dict(value.__dict__, current_indent + indent)
|
|
359
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
360
|
+
return format_sequence(value, current_indent + indent)
|
|
361
|
+
elif isinstance(value, bool):
|
|
362
|
+
return str(value).lower() if as_json else str(value)
|
|
363
|
+
elif isinstance(value, (int, float)):
|
|
364
|
+
return 'null' if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
|
|
365
|
+
elif isinstance(value, complex):
|
|
366
|
+
return f'[{value.real}, {value.imag}]' if as_json else str(value)
|
|
367
|
+
elif value is None:
|
|
368
|
+
return 'null' if as_json else 'None'
|
|
369
|
+
else:
|
|
370
|
+
return '"' + escape_string(str(value), '"') + '"' if as_json else "'" + escape_string(str(value), "'") + "'"
|
|
371
|
+
def should_expand(seq:list|tuple|dict) -> bool:
|
|
372
|
+
if compactness == 0: return True
|
|
373
|
+
if compactness == 2: return False
|
|
374
|
+
complex_items = sum(1 for item in seq if isinstance(item, (list, tuple, dict, set, frozenset)))
|
|
375
|
+
return complex_items > 1 or (complex_items == 1 and len(seq) > 1) or Data.chars_count(seq) + (len(seq) * len(sep)) > max_width
|
|
376
|
+
def format_key(k:any) -> str:
|
|
377
|
+
return '"' + escape_string(str(k), '"') + '"' if as_json else "'" + escape_string(str(k), "'") + "'" if isinstance(k, str) else str(k)
|
|
378
|
+
def format_dict(d:dict, current_indent:int) -> str:
|
|
379
|
+
if not d or compactness == 2:
|
|
380
|
+
return '{' + sep.join(f'{format_key(k)}: {format_value(v, current_indent)}' for k, v in d.items()) + '}'
|
|
381
|
+
if not should_expand(d.values()):
|
|
382
|
+
return '{' + sep.join(f'{format_key(k)}: {format_value(v, current_indent)}' for k, v in d.items()) + '}'
|
|
383
|
+
items = []
|
|
384
|
+
for key, value in d.items():
|
|
385
|
+
formatted_value = format_value(value, current_indent)
|
|
386
|
+
items.append(f'{" " * (current_indent + indent)}{format_key(key)}: {formatted_value}')
|
|
387
|
+
return '{\n' + ',\n'.join(items) + f'\n{" " * current_indent}}}'
|
|
388
|
+
def format_sequence(seq, current_indent:int) -> str:
|
|
389
|
+
if as_json:
|
|
390
|
+
seq = list(seq)
|
|
391
|
+
if not seq or compactness == 2:
|
|
392
|
+
return '[' + sep.join(format_value(item, current_indent) for item in seq) + ']' if isinstance(seq, list) else '(' + sep.join(format_value(item, current_indent) for item in seq) + ')'
|
|
393
|
+
if not should_expand(seq):
|
|
394
|
+
return '[' + sep.join(format_value(item, current_indent) for item in seq) + ']' if isinstance(seq, list) else '(' + sep.join(format_value(item, current_indent) for item in seq) + ')'
|
|
395
|
+
items = [format_value(item, current_indent) for item in seq]
|
|
396
|
+
formatted_items = ',\n'.join(f'{" " * (current_indent + indent)}{item}' for item in items)
|
|
397
|
+
if isinstance(seq, list):
|
|
398
|
+
return '[\n' + formatted_items + f'\n{" " * current_indent}]'
|
|
399
|
+
else:
|
|
400
|
+
return '(\n' + formatted_items + f'\n{" " * current_indent})'
|
|
401
|
+
return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _is_key(data:list|tuple|dict, path_id:str) -> bool:
|
|
405
|
+
"""Returns `True` if the path-id points to a key in `data` and `False` otherwise.\n
|
|
406
|
+
------------------------------------------------------------------------------------
|
|
407
|
+
Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
|
|
408
|
+
that was created before using `Object.get_path_id()`."""
|
|
409
|
+
def check_nested(data:list|tuple|dict, path:list[int]) -> bool:
|
|
410
|
+
for i, idx in enumerate(path):
|
|
411
|
+
if isinstance(data, dict):
|
|
412
|
+
keys = list(data.keys())
|
|
413
|
+
if i == len(path) - 1:
|
|
414
|
+
return True
|
|
415
|
+
data = data[keys[idx]]
|
|
416
|
+
elif isinstance(data, (list, tuple)):
|
|
417
|
+
return False
|
|
418
|
+
else:
|
|
419
|
+
raise TypeError(f'Unsupported type {type(data)} at path {path[:i+1]}')
|
|
420
|
+
return False
|
|
421
|
+
if isinstance(data, (list, tuple)):
|
|
422
|
+
return False
|
|
423
|
+
path = Data._sep_path_id(path_id)
|
|
424
|
+
return check_nested(data, path)
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def _sep_path_id(path_id:str) -> list[int]:
|
|
428
|
+
if path_id.count('>') != 1:
|
|
429
|
+
raise ValueError(f'Invalid path-id: {path_id}')
|
|
430
|
+
id_part_len, path_ids_str = int(path_id.split('>')[0]), path_id.split('>')[1]
|
|
431
|
+
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_vars.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions for modifying and checking the systems environment-variables:
|
|
3
|
+
- `EnvVars.get_paths()`
|
|
4
|
+
- `EnvVars.has_path()`
|
|
5
|
+
- `EnvVars.add_path()`
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .xx_data import *
|
|
10
|
+
from .xx_path import *
|
|
11
|
+
|
|
12
|
+
import os as _os
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnvVars:
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def get_paths(as_list:bool = False) -> str|list:
|
|
21
|
+
paths = _os.environ.get('PATH')
|
|
22
|
+
return paths.split(_os.pathsep) if as_list else paths
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def has_path(path:str = None, cwd:bool = False, base_dir:bool = False) -> bool:
|
|
26
|
+
if cwd: path = _os.getcwd()
|
|
27
|
+
if base_dir: path = Path.get(base_dir=True)
|
|
28
|
+
paths = EnvVars.get_paths()
|
|
29
|
+
return path in paths
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def __add_sort_paths(add_path:str, current_paths:str) -> str:
|
|
33
|
+
final_paths = Data.remove_empty_items(Data.remove_duplicates(f'{add_path};{current_paths}'.split(_os.pathsep)))
|
|
34
|
+
final_paths.sort()
|
|
35
|
+
return f'{_os.pathsep.join(final_paths)};'
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def add_path(add_path:str = None, cwd:bool = False, base_dir:bool = False, persistent:bool = True) -> None:
|
|
39
|
+
if cwd:
|
|
40
|
+
add_path = _os.getcwd()
|
|
41
|
+
if base_dir:
|
|
42
|
+
add_path = Path.get(base_dir=True)
|
|
43
|
+
if not EnvVars.has_path(add_path):
|
|
44
|
+
final_paths = EnvVars.__add_sort_paths(add_path, EnvVars.get_paths())
|
|
45
|
+
_os.environ['PATH'] = final_paths
|
|
46
|
+
if persistent:
|
|
47
|
+
if _os.name == 'nt': # Windows
|
|
48
|
+
try:
|
|
49
|
+
import winreg
|
|
50
|
+
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_ALL_ACCESS)
|
|
51
|
+
winreg.SetValueEx(key, 'PATH', 0, winreg.REG_EXPAND_SZ, final_paths)
|
|
52
|
+
winreg.CloseKey(key)
|
|
53
|
+
except ImportError: raise ImportError('Unable to make persistent changes on Windows.')
|
|
54
|
+
else: # UNIX-LIKE (Linux/macOS)
|
|
55
|
+
shell_rc_file = _os.path.expanduser('~/.bashrc' if _os.path.exists(_os.path.expanduser('~/.bashrc')) else '~/.zshrc')
|
|
56
|
+
with open(shell_rc_file, 'a') as f:
|
|
57
|
+
f.write(f'\n# Added by XulbuX\nexport PATH="$PATH:{add_path}"\n')
|
|
58
|
+
_os.system(f'source {shell_rc_file}')
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f'{add_path} is already in PATH.')
|
xulbux/xx_file.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from .xx_string import *
|
|
2
|
+
from .xx_path import *
|
|
3
|
+
|
|
4
|
+
import os as _os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class File:
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _make_path(filename:str, filetype:str, search_in:str|list[str] = None, prefer_base_dir:bool = True, correct_path:bool = False) -> str:
|
|
13
|
+
"""Get the path to a file in the cwd, the base-dir, or predefined directories.\n
|
|
14
|
+
--------------------------------------------------------------------------------------
|
|
15
|
+
If the `filename` is not found in the above directories, it will be searched<br>
|
|
16
|
+
in the `search_in` directory/directories. If the file is still not found, it will<br>
|
|
17
|
+
return the path to the file in the base-dir per default or to the file in the<br>
|
|
18
|
+
cwd if `prefer_base_dir` is set to `False`."""
|
|
19
|
+
if not filename.lower().endswith(f'.{filetype.lower()}'):
|
|
20
|
+
filename = f'{filename}.{filetype.lower()}'
|
|
21
|
+
try:
|
|
22
|
+
return Path.extend(filename, search_in, True, correct_path)
|
|
23
|
+
except FileNotFoundError:
|
|
24
|
+
return _os.path.join(Path.get(base_dir=True), filename) if prefer_base_dir else _os.path.join(_os.getcwd(), filename)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def rename_extension(file_path:str, new_extension:str) -> str:
|
|
28
|
+
directory, filename_with_ext = _os.path.split(file_path)
|
|
29
|
+
filename = filename_with_ext.split('.')[0]
|
|
30
|
+
camel_case_filename = String.to_camel_case(filename)
|
|
31
|
+
new_filename = f'{camel_case_filename}{new_extension}'
|
|
32
|
+
new_file_path = _os.path.join(directory, new_filename)
|
|
33
|
+
return new_file_path
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def create(content:str = '', file:str = 'new_file.txt', force:bool = False) -> str:
|
|
37
|
+
"""Create a file with ot without content.\n
|
|
38
|
+
----------------------------------------------------------------------------
|
|
39
|
+
The function will throw a `FileExistsError` if the file already exists.<br>
|
|
40
|
+
To overwrite the file, set the `force` parameter to `True`."""
|
|
41
|
+
if _os.path.exists(file) and not force:
|
|
42
|
+
with open(file, 'r', encoding='utf-8') as existing_file:
|
|
43
|
+
existing_content = existing_file.read()
|
|
44
|
+
if existing_content == content:
|
|
45
|
+
raise FileExistsError('Already created this file. (nothing changed)')
|
|
46
|
+
raise FileExistsError('File already exists.')
|
|
47
|
+
with open(file, 'w', encoding='utf-8') as f:
|
|
48
|
+
f.write(content)
|
|
49
|
+
full_path = _os.path.abspath(file)
|
|
50
|
+
return full_path
|