xulbux 1.5.5__py3-none-any.whl → 1.5.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 +20 -10
- xulbux/{__help__.py → _cli_.py} +15 -37
- xulbux/_consts_.py +108 -111
- xulbux/xx_cmd.py +245 -104
- xulbux/xx_code.py +28 -25
- xulbux/xx_color.py +330 -182
- xulbux/xx_data.py +214 -90
- xulbux/xx_env_vars.py +36 -23
- xulbux/xx_file.py +20 -14
- xulbux/xx_format_codes.py +154 -88
- xulbux/xx_json.py +36 -16
- xulbux/xx_path.py +38 -23
- xulbux/xx_regex.py +44 -27
- xulbux/xx_string.py +75 -47
- xulbux/xx_system.py +37 -26
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/METADATA +14 -10
- xulbux-1.5.7.dist-info/RECORD +20 -0
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/WHEEL +1 -1
- xulbux-1.5.7.dist-info/entry_points.txt +3 -0
- xulbux-1.5.5.dist-info/RECORD +0 -20
- xulbux-1.5.5.dist-info/entry_points.txt +0 -2
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/licenses/LICENSE +0 -0
xulbux/xx_data.py
CHANGED
|
@@ -2,19 +2,17 @@ import math as _math
|
|
|
2
2
|
import re as _re
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
5
|
class Data:
|
|
8
6
|
|
|
9
7
|
@staticmethod
|
|
10
|
-
def chars_count(data:list|tuple|set|frozenset|dict) -> int:
|
|
8
|
+
def chars_count(data: list | tuple | set | frozenset | dict) -> int:
|
|
11
9
|
"""The sum of all the characters including the keys in dictionaries."""
|
|
12
10
|
if isinstance(data, dict):
|
|
13
11
|
return sum(len(str(k)) + len(str(v)) for k, v in data.items())
|
|
14
12
|
return sum(len(str(item)) for item in data)
|
|
15
13
|
|
|
16
14
|
@staticmethod
|
|
17
|
-
def strip(data:list|tuple|dict) -> list|tuple|dict:
|
|
15
|
+
def strip(data: list | tuple | dict) -> list | tuple | dict:
|
|
18
16
|
if isinstance(data, dict):
|
|
19
17
|
return {k: v.strip() if isinstance(v, str) else Data.strip(v) for k, v in data.items()}
|
|
20
18
|
elif isinstance(data, (list, tuple)):
|
|
@@ -23,7 +21,7 @@ class Data:
|
|
|
23
21
|
return data.strip() if isinstance(data, str) else data
|
|
24
22
|
|
|
25
23
|
@staticmethod
|
|
26
|
-
def remove(data:list|tuple|dict, items:list[str]) -> list|tuple|dict:
|
|
24
|
+
def remove(data: list | tuple | dict, items: list[str]) -> list | tuple | dict:
|
|
27
25
|
"""Remove multiple items from lists and tuples or keys from dictionaries."""
|
|
28
26
|
if isinstance(data, (list, tuple)):
|
|
29
27
|
result = [k for k in data if k not in items]
|
|
@@ -32,14 +30,17 @@ class Data:
|
|
|
32
30
|
return {k: v for k, v in data.items() if k not in items}
|
|
33
31
|
|
|
34
32
|
@staticmethod
|
|
35
|
-
def remove_empty_items(data:list|tuple|dict, spaces_are_empty:bool = False) -> list|tuple|dict:
|
|
33
|
+
def remove_empty_items(data: list | tuple | dict, spaces_are_empty: bool = False) -> list | tuple | dict:
|
|
36
34
|
if isinstance(data, dict):
|
|
37
35
|
filtered_dict = {}
|
|
38
36
|
for key, value in data.items():
|
|
39
37
|
if isinstance(value, (list, tuple, dict)):
|
|
40
38
|
filtered_value = Data.remove_empty_items(value, spaces_are_empty)
|
|
41
|
-
if filtered_value:
|
|
42
|
-
|
|
39
|
+
if filtered_value:
|
|
40
|
+
filtered_dict[key] = filtered_value
|
|
41
|
+
elif value not in (None, "") and not (
|
|
42
|
+
(spaces_are_empty and isinstance(value, str)) and value.strip() in (None, "")
|
|
43
|
+
):
|
|
43
44
|
filtered_dict[key] = value
|
|
44
45
|
return filtered_dict
|
|
45
46
|
filtered = []
|
|
@@ -50,12 +51,12 @@ class Data:
|
|
|
50
51
|
if isinstance(item, tuple):
|
|
51
52
|
deduped_item = tuple(deduped_item)
|
|
52
53
|
filtered.append(deduped_item)
|
|
53
|
-
elif item not in (None,
|
|
54
|
+
elif item not in (None, "") and not ((spaces_are_empty and isinstance(item, str)) and item.strip() in (None, "")):
|
|
54
55
|
filtered.append(item)
|
|
55
56
|
return tuple(filtered) if isinstance(data, tuple) else filtered
|
|
56
57
|
|
|
57
58
|
@staticmethod
|
|
58
|
-
def remove_duplicates(data:list|tuple|dict) -> list|tuple|dict:
|
|
59
|
+
def remove_duplicates(data: list | tuple | dict) -> list | tuple | dict:
|
|
59
60
|
if isinstance(data, dict):
|
|
60
61
|
return {k: Data.remove_duplicates(v) for k, v in data.items()}
|
|
61
62
|
elif isinstance(data, (list, tuple)):
|
|
@@ -71,7 +72,12 @@ class Data:
|
|
|
71
72
|
return data
|
|
72
73
|
|
|
73
74
|
@staticmethod
|
|
74
|
-
def remove_comments(
|
|
75
|
+
def remove_comments(
|
|
76
|
+
data: list | tuple | dict,
|
|
77
|
+
comment_start: str = ">>",
|
|
78
|
+
comment_end: str = "<<",
|
|
79
|
+
comment_sep: str = "",
|
|
80
|
+
) -> list | tuple | dict:
|
|
75
81
|
"""Remove comments from a list, tuple or dictionary.\n
|
|
76
82
|
-----------------------------------------------------------------------------------------------------------------
|
|
77
83
|
The `data` parameter is your list, tuple or dictionary, where the comments should get removed from.<br>
|
|
@@ -109,8 +115,12 @@ class Data:
|
|
|
109
115
|
`value3` The comment is removed and the parts left and right are joined through `comment_sep`.<br>
|
|
110
116
|
`value4` The whole value is removed, since the whole value was a comment.<br>
|
|
111
117
|
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
|
-
|
|
118
|
+
For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def process_item(
|
|
122
|
+
item: dict | list | tuple | str,
|
|
123
|
+
) -> dict | list | tuple | str | None:
|
|
114
124
|
if isinstance(item, dict):
|
|
115
125
|
processed_dict = {}
|
|
116
126
|
for key, val in item.items():
|
|
@@ -118,7 +128,8 @@ class Data:
|
|
|
118
128
|
if processed_key is not None:
|
|
119
129
|
processed_val = process_item(val)
|
|
120
130
|
if isinstance(val, (list, tuple, dict)):
|
|
121
|
-
if processed_val:
|
|
131
|
+
if processed_val:
|
|
132
|
+
processed_dict[processed_key] = processed_val
|
|
122
133
|
elif processed_val is not None:
|
|
123
134
|
processed_dict[processed_key] = processed_val
|
|
124
135
|
else:
|
|
@@ -130,17 +141,28 @@ class Data:
|
|
|
130
141
|
return tuple(v for v in (process_item(val) for val in item) if v is not None)
|
|
131
142
|
elif isinstance(item, str):
|
|
132
143
|
if comment_end:
|
|
133
|
-
no_comments = _re.sub(
|
|
134
|
-
|
|
144
|
+
no_comments = _re.sub(
|
|
145
|
+
rf"^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$",
|
|
146
|
+
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()}',
|
|
147
|
+
item,
|
|
148
|
+
)
|
|
135
149
|
else:
|
|
136
150
|
no_comments = None if item.lstrip().startswith(comment_start) else item
|
|
137
|
-
return no_comments.strip() if no_comments and no_comments.strip() !=
|
|
151
|
+
return no_comments.strip() if no_comments and no_comments.strip() != "" else None
|
|
138
152
|
else:
|
|
139
153
|
return item
|
|
154
|
+
|
|
140
155
|
return process_item(data)
|
|
141
156
|
|
|
142
157
|
@staticmethod
|
|
143
|
-
def is_equal(
|
|
158
|
+
def is_equal(
|
|
159
|
+
data1: list | tuple | dict,
|
|
160
|
+
data2: list | tuple | dict,
|
|
161
|
+
ignore_paths: str | list[str] = "",
|
|
162
|
+
comment_start: str = ">>",
|
|
163
|
+
comment_end: str = "<<",
|
|
164
|
+
sep: str = "->",
|
|
165
|
+
) -> bool:
|
|
144
166
|
"""Compares two structures and returns `True` if they are equal and `False` otherwise.\n
|
|
145
167
|
⇾ **Will not detect, if a key-name has changed, only if removed or added.**\n
|
|
146
168
|
------------------------------------------------------------------------------------------------
|
|
@@ -149,13 +171,25 @@ class Data:
|
|
|
149
171
|
------------------------------------------------------------------------------------------------
|
|
150
172
|
The paths from `ignore_paths` work exactly the same way as the paths from `value_paths`<br>
|
|
151
173
|
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
|
-
|
|
174
|
+
explanation, see the documentation of the function `Data.get_path_id()`.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def process_ignore_paths(
|
|
178
|
+
ignore_paths: str | list[str],
|
|
179
|
+
) -> list[list[str]]:
|
|
154
180
|
if isinstance(ignore_paths, str):
|
|
155
181
|
ignore_paths = [ignore_paths]
|
|
156
182
|
return [path.split(sep) for path in ignore_paths if path]
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
|
|
184
|
+
def compare(
|
|
185
|
+
d1: dict | list | tuple,
|
|
186
|
+
d2: dict | list | tuple,
|
|
187
|
+
ignore_paths: list[list[str]],
|
|
188
|
+
current_path: list = [],
|
|
189
|
+
) -> bool:
|
|
190
|
+
if ignore_paths and any(
|
|
191
|
+
current_path == path[: len(current_path)] and len(current_path) == len(path) for path in ignore_paths
|
|
192
|
+
):
|
|
159
193
|
return True
|
|
160
194
|
if isinstance(d1, dict) and isinstance(d2, dict):
|
|
161
195
|
if set(d1.keys()) != set(d2.keys()):
|
|
@@ -164,13 +198,23 @@ class Data:
|
|
|
164
198
|
elif isinstance(d1, (list, tuple)) and isinstance(d2, (list, tuple)):
|
|
165
199
|
if len(d1) != len(d2):
|
|
166
200
|
return False
|
|
167
|
-
return all(
|
|
201
|
+
return all(
|
|
202
|
+
compare(item1, item2, ignore_paths, current_path + [str(i)])
|
|
203
|
+
for i, (item1, item2) in enumerate(zip(d1, d2))
|
|
204
|
+
)
|
|
168
205
|
else:
|
|
169
206
|
return d1 == d2
|
|
170
|
-
|
|
207
|
+
|
|
208
|
+
return compare(
|
|
209
|
+
Data.remove_comments(data1, comment_start, comment_end),
|
|
210
|
+
Data.remove_comments(data2, comment_start, comment_end),
|
|
211
|
+
process_ignore_paths(ignore_paths),
|
|
212
|
+
)
|
|
171
213
|
|
|
172
214
|
@staticmethod
|
|
173
|
-
def get_fingerprint(
|
|
215
|
+
def get_fingerprint(
|
|
216
|
+
data: list | tuple | dict,
|
|
217
|
+
) -> list | tuple | dict | None:
|
|
174
218
|
if isinstance(data, dict):
|
|
175
219
|
return {i: type(v).__name__ for i, v in enumerate(data.values())}
|
|
176
220
|
elif isinstance(data, (list, tuple)):
|
|
@@ -178,7 +222,12 @@ class Data:
|
|
|
178
222
|
return None
|
|
179
223
|
|
|
180
224
|
@staticmethod
|
|
181
|
-
def get_path_id(
|
|
225
|
+
def get_path_id(
|
|
226
|
+
data: list | tuple | dict,
|
|
227
|
+
value_paths: str | list[str],
|
|
228
|
+
sep: str = "->",
|
|
229
|
+
ignore_not_found: bool = False,
|
|
230
|
+
) -> str | list[str]:
|
|
182
231
|
"""Generates a unique ID based on the path to a specific value within a nested data structure.\n
|
|
183
232
|
-------------------------------------------------------------------------------------------------
|
|
184
233
|
The `data` parameter is the list, tuple, or dictionary, which the id should be generated for.\n
|
|
@@ -200,16 +249,17 @@ class Data:
|
|
|
200
249
|
-------------------------------------------------------------------------------------------------
|
|
201
250
|
If `ignore_not_found` is `True`, the function will return `None` if the value is not<br>
|
|
202
251
|
found instead of raising an error."""
|
|
203
|
-
if isinstance(value_paths, str):
|
|
252
|
+
if isinstance(value_paths, str):
|
|
253
|
+
value_paths = [value_paths]
|
|
204
254
|
path_ids = []
|
|
205
255
|
for path in value_paths:
|
|
206
|
-
keys = [k.strip() for k in path.split(str(sep).strip()) if k.strip() !=
|
|
256
|
+
keys = [k.strip() for k in path.split(str(sep).strip()) if k.strip() != ""]
|
|
207
257
|
id_part_len, _path_ids, _obj = 0, [], data
|
|
208
258
|
try:
|
|
209
259
|
for k in keys:
|
|
210
260
|
if isinstance(_obj, dict):
|
|
211
261
|
if k.isdigit():
|
|
212
|
-
raise TypeError(f
|
|
262
|
+
raise TypeError(f"Key '{k}' is invalid for a dict type.")
|
|
213
263
|
try:
|
|
214
264
|
idx = list(_obj.keys()).index(k)
|
|
215
265
|
_path_ids.append(idx)
|
|
@@ -218,7 +268,7 @@ class Data:
|
|
|
218
268
|
if ignore_not_found:
|
|
219
269
|
_path_ids = None
|
|
220
270
|
break
|
|
221
|
-
raise KeyError(f
|
|
271
|
+
raise KeyError(f"Key '{k}' not found in dict.")
|
|
222
272
|
elif isinstance(_obj, (list, tuple)):
|
|
223
273
|
try:
|
|
224
274
|
idx = int(k)
|
|
@@ -233,7 +283,7 @@ class Data:
|
|
|
233
283
|
if ignore_not_found:
|
|
234
284
|
_path_ids = None
|
|
235
285
|
break
|
|
236
|
-
raise ValueError(f
|
|
286
|
+
raise ValueError(f"Value '{k}' not found in list/tuple.")
|
|
237
287
|
else:
|
|
238
288
|
break
|
|
239
289
|
if _path_ids:
|
|
@@ -245,11 +295,12 @@ class Data:
|
|
|
245
295
|
except (KeyError, ValueError, TypeError) as e:
|
|
246
296
|
if ignore_not_found:
|
|
247
297
|
path_ids.append(None)
|
|
248
|
-
else:
|
|
298
|
+
else:
|
|
299
|
+
raise e
|
|
249
300
|
return path_ids if len(path_ids) > 1 else path_ids[0] if len(path_ids) == 1 else None
|
|
250
301
|
|
|
251
302
|
@staticmethod
|
|
252
|
-
def get_value_by_path_id(data:list|tuple|dict, path_id:str, get_key:bool = False) -> any:
|
|
303
|
+
def get_value_by_path_id(data: list | tuple | dict, path_id: str, get_key: bool = False) -> any:
|
|
253
304
|
"""Retrieves the value from `data` using the provided `path_id`.\n
|
|
254
305
|
------------------------------------------------------------------------------------
|
|
255
306
|
Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
|
|
@@ -257,8 +308,10 @@ class Data:
|
|
|
257
308
|
and the final item is in a dict, it returns the key instead of the value.\n
|
|
258
309
|
------------------------------------------------------------------------------------
|
|
259
310
|
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
|
-
|
|
311
|
+
the structure of `data` hasn't changed since creating the path-id to that value.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
def get_nested(data: list | tuple | dict, path: list[int], get_key: bool) -> any:
|
|
262
315
|
parent = None
|
|
263
316
|
for i, idx in enumerate(path):
|
|
264
317
|
if isinstance(data, dict):
|
|
@@ -270,18 +323,23 @@ class Data:
|
|
|
270
323
|
elif isinstance(data, (list, tuple)):
|
|
271
324
|
if i == len(path) - 1 and get_key:
|
|
272
325
|
if parent is None or not isinstance(parent, dict):
|
|
273
|
-
raise ValueError(
|
|
326
|
+
raise ValueError("Cannot get key from list or tuple without a parent dictionary")
|
|
274
327
|
return next(key for key, value in parent.items() if value is data)
|
|
275
328
|
parent = data
|
|
276
329
|
data = data[idx]
|
|
277
330
|
else:
|
|
278
|
-
raise TypeError(f
|
|
331
|
+
raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
|
|
279
332
|
return data
|
|
333
|
+
|
|
280
334
|
path = Data._sep_path_id(path_id)
|
|
281
335
|
return get_nested(data, path, get_key)
|
|
282
336
|
|
|
283
337
|
@staticmethod
|
|
284
|
-
def set_value_by_path_id(
|
|
338
|
+
def set_value_by_path_id(
|
|
339
|
+
data: list | tuple | dict,
|
|
340
|
+
update_values: str | list[str],
|
|
341
|
+
sep: str = "::",
|
|
342
|
+
) -> list | tuple | dict:
|
|
285
343
|
"""Updates the value/s from `update_values` in the `data`.\n
|
|
286
344
|
--------------------------------------------------------------------------------
|
|
287
345
|
Input a list, tuple or dict as `data`, along with `update_values`, which is<br>
|
|
@@ -290,8 +348,10 @@ class Data:
|
|
|
290
348
|
and the new value are separated by `sep`, which per default is `::`.\n
|
|
291
349
|
--------------------------------------------------------------------------------
|
|
292
350
|
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
|
-
|
|
351
|
+
structure of `data` hasn't changed since creating the path-id to that value.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def update_nested(data: list | tuple | dict, path: list[int], value: any) -> list | tuple | dict:
|
|
295
355
|
if len(path) == 1:
|
|
296
356
|
if isinstance(data, dict):
|
|
297
357
|
keys = list(data.keys())
|
|
@@ -309,33 +369,58 @@ class Data:
|
|
|
309
369
|
data[path[0]] = update_nested(data[path[0]], path[1:], value)
|
|
310
370
|
data = type(data)(data)
|
|
311
371
|
return data
|
|
372
|
+
|
|
312
373
|
if isinstance(update_values, str):
|
|
313
374
|
update_values = [update_values]
|
|
314
|
-
valid_entries = [
|
|
375
|
+
valid_entries = [
|
|
376
|
+
(parts[0].strip(), parts[1])
|
|
377
|
+
for update_value in update_values
|
|
378
|
+
if len(parts := update_value.split(str(sep).strip())) == 2
|
|
379
|
+
]
|
|
315
380
|
if not valid_entries:
|
|
316
|
-
raise ValueError(f
|
|
317
|
-
path, new_values =
|
|
381
|
+
raise ValueError(f"No valid update_values found: {update_values}")
|
|
382
|
+
path, new_values = zip(*valid_entries) if valid_entries else ([], [])
|
|
318
383
|
for path_id, new_val in zip(path, new_values):
|
|
319
384
|
path = Data._sep_path_id(path_id)
|
|
320
385
|
data = update_nested(data, path, new_val)
|
|
321
386
|
return data
|
|
322
387
|
|
|
323
388
|
@staticmethod
|
|
324
|
-
def print(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
389
|
+
def print(
|
|
390
|
+
data: list | tuple | dict,
|
|
391
|
+
indent: int = 2,
|
|
392
|
+
compactness: int = 1,
|
|
393
|
+
sep: str = ", ",
|
|
394
|
+
max_width: int = 140,
|
|
395
|
+
as_json: bool = False,
|
|
396
|
+
end: str = "\n",
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Print nicely formatted data structures.\n
|
|
399
|
+
------------------------------------------------------------------------------------
|
|
400
|
+
The indentation spaces-amount can be set with with `indent`.<br>
|
|
401
|
+
There are three different levels of `compactness`:<br>
|
|
402
|
+
`0` expands everything possible<br>
|
|
403
|
+
`1` only expands if there's other lists, tuples or dicts inside of data or,<br>
|
|
404
|
+
⠀if the data's content is longer than `max_width`<br>
|
|
405
|
+
`2` keeps everything collapsed (all on one line)\n
|
|
406
|
+
------------------------------------------------------------------------------------
|
|
407
|
+
If `as_json` is set to `True`, the output will be in valid JSON format.
|
|
408
|
+
"""
|
|
409
|
+
print(
|
|
410
|
+
Data.to_str(data, indent, compactness, sep, max_width, as_json),
|
|
411
|
+
end=end,
|
|
412
|
+
flush=True,
|
|
413
|
+
)
|
|
336
414
|
|
|
337
415
|
@staticmethod
|
|
338
|
-
def to_str(
|
|
416
|
+
def to_str(
|
|
417
|
+
data: list | tuple | dict,
|
|
418
|
+
indent: int = 2,
|
|
419
|
+
compactness: int = 1,
|
|
420
|
+
sep: str = ", ",
|
|
421
|
+
max_width: int = 140,
|
|
422
|
+
as_json: bool = False,
|
|
423
|
+
) -> str:
|
|
339
424
|
"""Get nicely formatted data structure-strings.\n
|
|
340
425
|
------------------------------------------------------------------------------------
|
|
341
426
|
The indentation spaces-amount can be set with with `indent`.<br>
|
|
@@ -345,68 +430,105 @@ class Data:
|
|
|
345
430
|
⠀if the data's content is longer than `max_width`<br>
|
|
346
431
|
`2` keeps everything collapsed (all on one line)\n
|
|
347
432
|
------------------------------------------------------------------------------------
|
|
348
|
-
If `as_json` is set to `True`, the output will be in valid JSON format.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
433
|
+
If `as_json` is set to `True`, the output will be in valid JSON format.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def escape_string(s: str, str_quotes: str = '"') -> str:
|
|
437
|
+
s = (
|
|
438
|
+
s.replace("\\", r"\\")
|
|
439
|
+
.replace("\n", r"\n")
|
|
440
|
+
.replace("\r", r"\r")
|
|
441
|
+
.replace("\t", r"\t")
|
|
442
|
+
.replace("\b", r"\b")
|
|
443
|
+
.replace("\f", r"\f")
|
|
444
|
+
.replace("\a", r"\a")
|
|
445
|
+
)
|
|
446
|
+
if str_quotes == '"':
|
|
447
|
+
s = s.replace(r"\\'", "'").replace(r'"', r"\"")
|
|
448
|
+
elif str_quotes == "'":
|
|
449
|
+
s = s.replace(r'\\"', '"').replace(r"'", r"\'")
|
|
353
450
|
return s
|
|
354
|
-
|
|
451
|
+
|
|
452
|
+
def format_value(value: any, current_indent: int) -> str:
|
|
355
453
|
if isinstance(value, dict):
|
|
356
454
|
return format_dict(value, current_indent + indent)
|
|
357
|
-
elif hasattr(value,
|
|
455
|
+
elif hasattr(value, "__dict__"):
|
|
358
456
|
return format_dict(value.__dict__, current_indent + indent)
|
|
359
457
|
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
360
458
|
return format_sequence(value, current_indent + indent)
|
|
361
459
|
elif isinstance(value, bool):
|
|
362
460
|
return str(value).lower() if as_json else str(value)
|
|
363
461
|
elif isinstance(value, (int, float)):
|
|
364
|
-
return
|
|
462
|
+
return "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
|
|
365
463
|
elif isinstance(value, complex):
|
|
366
|
-
return f
|
|
464
|
+
return f"[{value.real}, {value.imag}]" if as_json else str(value)
|
|
367
465
|
elif value is None:
|
|
368
|
-
return
|
|
466
|
+
return "null" if as_json else "None"
|
|
369
467
|
else:
|
|
370
468
|
return '"' + escape_string(str(value), '"') + '"' if as_json else "'" + escape_string(str(value), "'") + "'"
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if compactness ==
|
|
469
|
+
|
|
470
|
+
def should_expand(seq: list | tuple | dict) -> bool:
|
|
471
|
+
if compactness == 0:
|
|
472
|
+
return True
|
|
473
|
+
if compactness == 2:
|
|
474
|
+
return False
|
|
374
475
|
complex_items = sum(1 for item in seq if isinstance(item, (list, tuple, dict, set, frozenset)))
|
|
375
|
-
return
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
476
|
+
return (
|
|
477
|
+
complex_items > 1
|
|
478
|
+
or (complex_items == 1 and len(seq) > 1)
|
|
479
|
+
or Data.chars_count(seq) + (len(seq) * len(sep)) > max_width
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def format_key(k: any) -> str:
|
|
483
|
+
return (
|
|
484
|
+
'"' + escape_string(str(k), '"') + '"'
|
|
485
|
+
if as_json
|
|
486
|
+
else ("'" + escape_string(str(k), "'") + "'" if isinstance(k, str) else str(k))
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def format_dict(d: dict, current_indent: int) -> str:
|
|
379
490
|
if not d or compactness == 2:
|
|
380
|
-
return
|
|
491
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
381
492
|
if not should_expand(d.values()):
|
|
382
|
-
return
|
|
493
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
383
494
|
items = []
|
|
384
495
|
for key, value in d.items():
|
|
385
496
|
formatted_value = format_value(value, current_indent)
|
|
386
497
|
items.append(f'{" " * (current_indent + indent)}{format_key(key)}: {formatted_value}')
|
|
387
|
-
return
|
|
388
|
-
|
|
498
|
+
return "{\n" + ",\n".join(items) + f'\n{" " * current_indent}}}'
|
|
499
|
+
|
|
500
|
+
def format_sequence(seq, current_indent: int) -> str:
|
|
389
501
|
if as_json:
|
|
390
502
|
seq = list(seq)
|
|
391
503
|
if not seq or compactness == 2:
|
|
392
|
-
return
|
|
504
|
+
return (
|
|
505
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
506
|
+
if isinstance(seq, list)
|
|
507
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
508
|
+
)
|
|
393
509
|
if not should_expand(seq):
|
|
394
|
-
return
|
|
510
|
+
return (
|
|
511
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
512
|
+
if isinstance(seq, list)
|
|
513
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
514
|
+
)
|
|
395
515
|
items = [format_value(item, current_indent) for item in seq]
|
|
396
|
-
formatted_items =
|
|
516
|
+
formatted_items = ",\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
|
|
397
517
|
if isinstance(seq, list):
|
|
398
|
-
return
|
|
518
|
+
return "[\n" + formatted_items + f'\n{" " * current_indent}]'
|
|
399
519
|
else:
|
|
400
|
-
return
|
|
520
|
+
return "(\n" + formatted_items + f'\n{" " * current_indent})'
|
|
521
|
+
|
|
401
522
|
return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
|
|
402
523
|
|
|
403
524
|
@staticmethod
|
|
404
|
-
def _is_key(data:list|tuple|dict, path_id:str) -> bool:
|
|
525
|
+
def _is_key(data: list | tuple | dict, path_id: str) -> bool:
|
|
405
526
|
"""Returns `True` if the path-id points to a key in `data` and `False` otherwise.\n
|
|
406
527
|
------------------------------------------------------------------------------------
|
|
407
528
|
Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
|
|
408
529
|
that was created before using `Object.get_path_id()`."""
|
|
409
|
-
|
|
530
|
+
|
|
531
|
+
def check_nested(data: list | tuple | dict, path: list[int]) -> bool:
|
|
410
532
|
for i, idx in enumerate(path):
|
|
411
533
|
if isinstance(data, dict):
|
|
412
534
|
keys = list(data.keys())
|
|
@@ -416,16 +538,18 @@ class Data:
|
|
|
416
538
|
elif isinstance(data, (list, tuple)):
|
|
417
539
|
return False
|
|
418
540
|
else:
|
|
419
|
-
raise TypeError(f
|
|
541
|
+
raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
|
|
420
542
|
return False
|
|
543
|
+
|
|
421
544
|
if isinstance(data, (list, tuple)):
|
|
422
545
|
return False
|
|
423
546
|
path = Data._sep_path_id(path_id)
|
|
424
547
|
return check_nested(data, path)
|
|
425
548
|
|
|
426
549
|
@staticmethod
|
|
427
|
-
def _sep_path_id(path_id:str) -> list[int]:
|
|
428
|
-
if path_id.count(
|
|
429
|
-
raise ValueError(f
|
|
430
|
-
id_part_len
|
|
431
|
-
|
|
550
|
+
def _sep_path_id(path_id: str) -> list[int]:
|
|
551
|
+
if path_id.count(">") != 1:
|
|
552
|
+
raise ValueError(f"Invalid path-id: {path_id}")
|
|
553
|
+
id_part_len = int(path_id.split(">")[0])
|
|
554
|
+
path_ids_str = path_id.split(">")[1]
|
|
555
|
+
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
CHANGED
|
@@ -5,56 +5,69 @@ Functions for modifying and checking the systems environment-variables:
|
|
|
5
5
|
- `EnvVars.add_path()`
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
from .xx_data import *
|
|
10
9
|
from .xx_path import *
|
|
11
10
|
|
|
12
11
|
import os as _os
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
14
|
class EnvVars:
|
|
18
15
|
|
|
19
16
|
@staticmethod
|
|
20
|
-
def get_paths(as_list:bool = False) -> str|list:
|
|
21
|
-
paths = _os.environ.get(
|
|
17
|
+
def get_paths(as_list: bool = False) -> str | list:
|
|
18
|
+
paths = _os.environ.get("PATH")
|
|
22
19
|
return paths.split(_os.pathsep) if as_list else paths
|
|
23
20
|
|
|
24
21
|
@staticmethod
|
|
25
|
-
def has_path(path:str = None, cwd:bool = False, base_dir:bool = False) -> bool:
|
|
26
|
-
if cwd:
|
|
27
|
-
|
|
22
|
+
def has_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> bool:
|
|
23
|
+
if cwd:
|
|
24
|
+
path = _os.getcwd()
|
|
25
|
+
if base_dir:
|
|
26
|
+
path = Path.get(base_dir=True)
|
|
28
27
|
paths = EnvVars.get_paths()
|
|
29
28
|
return path in paths
|
|
30
29
|
|
|
31
30
|
@staticmethod
|
|
32
|
-
def __add_sort_paths(add_path:str, current_paths:str) -> str:
|
|
33
|
-
final_paths = Data.remove_empty_items(Data.remove_duplicates(f
|
|
31
|
+
def __add_sort_paths(add_path: str, current_paths: str) -> str:
|
|
32
|
+
final_paths = Data.remove_empty_items(Data.remove_duplicates(f"{add_path};{current_paths}".split(_os.pathsep)))
|
|
34
33
|
final_paths.sort()
|
|
35
|
-
return f
|
|
34
|
+
return f"{_os.pathsep.join(final_paths)};"
|
|
36
35
|
|
|
37
36
|
@staticmethod
|
|
38
|
-
def add_path(
|
|
37
|
+
def add_path(
|
|
38
|
+
add_path: str = None,
|
|
39
|
+
cwd: bool = False,
|
|
40
|
+
base_dir: bool = False,
|
|
41
|
+
persistent: bool = True,
|
|
42
|
+
) -> None:
|
|
39
43
|
if cwd:
|
|
40
44
|
add_path = _os.getcwd()
|
|
41
45
|
if base_dir:
|
|
42
46
|
add_path = Path.get(base_dir=True)
|
|
43
47
|
if not EnvVars.has_path(add_path):
|
|
44
48
|
final_paths = EnvVars.__add_sort_paths(add_path, EnvVars.get_paths())
|
|
45
|
-
_os.environ[
|
|
49
|
+
_os.environ["PATH"] = final_paths
|
|
46
50
|
if persistent:
|
|
47
|
-
if _os.name ==
|
|
51
|
+
if _os.name == "nt": # Windows
|
|
48
52
|
try:
|
|
49
|
-
import winreg
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
import winreg as _winreg
|
|
54
|
+
|
|
55
|
+
key = _winreg.OpenKey(
|
|
56
|
+
_winreg.HKEY_CURRENT_USER,
|
|
57
|
+
"Environment",
|
|
58
|
+
0,
|
|
59
|
+
_winreg.KEY_ALL_ACCESS,
|
|
60
|
+
)
|
|
61
|
+
_winreg.SetValueEx(key, "PATH", 0, _winreg.REG_EXPAND_SZ, final_paths)
|
|
62
|
+
_winreg.CloseKey(key)
|
|
63
|
+
except ImportError:
|
|
64
|
+
raise ImportError("Unable to make persistent changes on Windows.")
|
|
54
65
|
else: # UNIX-LIKE (Linux/macOS)
|
|
55
|
-
shell_rc_file = _os.path.expanduser(
|
|
56
|
-
|
|
66
|
+
shell_rc_file = _os.path.expanduser(
|
|
67
|
+
"~/.bashrc" if _os.path.exists(_os.path.expanduser("~/.bashrc")) else "~/.zshrc"
|
|
68
|
+
)
|
|
69
|
+
with open(shell_rc_file, "a") as f:
|
|
57
70
|
f.write(f'\n# Added by XulbuX\nexport PATH="$PATH:{add_path}"\n')
|
|
58
|
-
_os.system(f
|
|
71
|
+
_os.system(f"source {shell_rc_file}")
|
|
59
72
|
else:
|
|
60
|
-
raise ValueError(f
|
|
73
|
+
raise ValueError(f"{add_path} is already in PATH.")
|