xulbux 1.6.1__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 +57 -0
- xulbux/_cli_.py +53 -0
- xulbux/_consts_.py +145 -0
- xulbux/xx_code.py +105 -0
- xulbux/xx_color.py +955 -0
- xulbux/xx_console.py +378 -0
- xulbux/xx_data.py +531 -0
- xulbux/xx_env_path.py +113 -0
- xulbux/xx_file.py +65 -0
- xulbux/xx_format_codes.py +305 -0
- xulbux/xx_json.py +106 -0
- xulbux/xx_path.py +107 -0
- xulbux/xx_regex.py +156 -0
- xulbux/xx_string.py +159 -0
- xulbux/xx_system.py +85 -0
- xulbux-1.6.1.dist-info/LICENSE +21 -0
- xulbux-1.6.1.dist-info/METADATA +110 -0
- xulbux-1.6.1.dist-info/RECORD +21 -0
- xulbux-1.6.1.dist-info/WHEEL +5 -0
- xulbux-1.6.1.dist-info/entry_points.txt +3 -0
- xulbux-1.6.1.dist-info/top_level.txt +1 -0
xulbux/xx_data.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
from .xx_string import String
|
|
2
|
+
|
|
3
|
+
from typing import TypeAlias, Union
|
|
4
|
+
import math as _math
|
|
5
|
+
import re as _re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Data:
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def chars_count(data: DataStructure) -> int:
|
|
15
|
+
"""The sum of all the characters amount including the keys in dictionaries."""
|
|
16
|
+
if isinstance(data, dict):
|
|
17
|
+
return sum(len(str(k)) + len(str(v)) for k, v in data.items())
|
|
18
|
+
return sum(len(str(item)) for item in data)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def strip(data: DataStructure) -> DataStructure:
|
|
22
|
+
"""Removes leading and trailing whitespaces from the data structure's items."""
|
|
23
|
+
if isinstance(data, dict):
|
|
24
|
+
return {k: Data.strip(v) for k, v in data.items()}
|
|
25
|
+
return type(data)(map(Data.strip, data))
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def remove_empty_items(data: DataStructure, spaces_are_empty: bool = False) -> DataStructure:
|
|
29
|
+
"""Removes empty items from the data structure.
|
|
30
|
+
If `spaces_are_empty` is true, it will count items with only spaces as empty."""
|
|
31
|
+
if isinstance(data, dict):
|
|
32
|
+
return {
|
|
33
|
+
k: (
|
|
34
|
+
v
|
|
35
|
+
if not isinstance(v, (list, tuple, set, frozenset, dict))
|
|
36
|
+
else Data.remove_empty_items(v, spaces_are_empty)
|
|
37
|
+
)
|
|
38
|
+
for k, v in data.items()
|
|
39
|
+
if not String.is_empty(v, spaces_are_empty)
|
|
40
|
+
}
|
|
41
|
+
if isinstance(data, (list, tuple, set, frozenset)):
|
|
42
|
+
return type(data)(
|
|
43
|
+
item
|
|
44
|
+
for item in (
|
|
45
|
+
(
|
|
46
|
+
item
|
|
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())
|
|
54
|
+
)
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def remove_duplicates(data: DataStructure) -> DataStructure:
|
|
59
|
+
"""Removes all duplicates from the data structure."""
|
|
60
|
+
if isinstance(data, dict):
|
|
61
|
+
return {k: Data.remove_duplicates(v) for k, v in data.items()}
|
|
62
|
+
if isinstance(data, (list, tuple)):
|
|
63
|
+
return type(data)(
|
|
64
|
+
Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
|
|
65
|
+
for item in dict.fromkeys(data)
|
|
66
|
+
)
|
|
67
|
+
if isinstance(data, (set, frozenset)):
|
|
68
|
+
return type(data)(
|
|
69
|
+
Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
|
|
70
|
+
for item in data
|
|
71
|
+
)
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def remove_comments(
|
|
76
|
+
data: DataStructure,
|
|
77
|
+
comment_start: str = ">>",
|
|
78
|
+
comment_end: str = "<<",
|
|
79
|
+
comment_sep: str = "",
|
|
80
|
+
) -> DataStructure:
|
|
81
|
+
"""Remove comments from a list, tuple or dictionary.\n
|
|
82
|
+
----------------------------------------------------------------------------------------------------------------------
|
|
83
|
+
- The `data` parameter is your list, tuple or dictionary, where the comments should get removed from.
|
|
84
|
+
- The `comment_start` parameter is the string that marks the start of a comment inside `data`. (default: `>>`)
|
|
85
|
+
- The `comment_end` parameter is the string that marks the end of a comment inside `data`. (default: `<<`)
|
|
86
|
+
- The `comment_sep` parameter is a string with which a comment will be replaced, if it is in the middle of a value.\n
|
|
87
|
+
----------------------------------------------------------------------------------------------------------------------
|
|
88
|
+
Examples:
|
|
89
|
+
```python
|
|
90
|
+
data = {
|
|
91
|
+
"key1": [
|
|
92
|
+
">> COMMENT IN THE BEGINNING OF THE STRING << value1",
|
|
93
|
+
"value2 >> COMMENT IN THE END OF THE STRING",
|
|
94
|
+
"val>> COMMENT IN THE MIDDLE OF THE STRING <<ue3",
|
|
95
|
+
">> FULL VALUE IS A COMMENT value4"
|
|
96
|
+
],
|
|
97
|
+
">> FULL KEY + ALL ITS VALUES ARE A COMMENT key2": [
|
|
98
|
+
"value",
|
|
99
|
+
"value",
|
|
100
|
+
"value"
|
|
101
|
+
],
|
|
102
|
+
"key3": ">> ALL THE KEYS VALUES ARE COMMENTS value"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
processed_data = Data.remove_comments(
|
|
106
|
+
data,
|
|
107
|
+
comment_start=">>",
|
|
108
|
+
comment_end="<<",
|
|
109
|
+
comment_sep="__"
|
|
110
|
+
)
|
|
111
|
+
```\n
|
|
112
|
+
----------------------------------------------------------------------------------------------------------------------
|
|
113
|
+
For this example, `processed_data` will be:
|
|
114
|
+
```python
|
|
115
|
+
{
|
|
116
|
+
"key1": [
|
|
117
|
+
"value1",
|
|
118
|
+
"value2",
|
|
119
|
+
"val__ue3"
|
|
120
|
+
],
|
|
121
|
+
"key3": None
|
|
122
|
+
}
|
|
123
|
+
```\n
|
|
124
|
+
- For `key1`, all the comments will just be removed, except at `value3` and `value4`:
|
|
125
|
+
- `value3` The comment is removed and the parts left and right are joined through `comment_sep`.
|
|
126
|
+
- `value4` The whole value is removed, since the whole value was a comment.
|
|
127
|
+
- For `key2`, the key, including its whole values will be removed.
|
|
128
|
+
- For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`."""
|
|
129
|
+
if comment_end:
|
|
130
|
+
pattern = _re.compile(
|
|
131
|
+
rf"^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def process_string(s: str) -> str | None:
|
|
135
|
+
if comment_end:
|
|
136
|
+
match = pattern.match(s)
|
|
137
|
+
if match:
|
|
138
|
+
start, end = match.group(1).strip(), match.group(2).strip()
|
|
139
|
+
return f"{start}{comment_sep if start and end else ''}{end}" or None
|
|
140
|
+
return s.strip() or None
|
|
141
|
+
else:
|
|
142
|
+
return None if s.lstrip().startswith(comment_start) else s.strip() or None
|
|
143
|
+
|
|
144
|
+
def process_item(item: any) -> any:
|
|
145
|
+
if isinstance(item, dict):
|
|
146
|
+
return {
|
|
147
|
+
k: v for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
|
|
148
|
+
}
|
|
149
|
+
if isinstance(item, (list, tuple, set, frozenset)):
|
|
150
|
+
processed = (v for v in map(process_item, item) if v is not None)
|
|
151
|
+
return type(item)(processed)
|
|
152
|
+
if isinstance(item, str):
|
|
153
|
+
return process_string(item)
|
|
154
|
+
return item
|
|
155
|
+
|
|
156
|
+
return process_item(data)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def is_equal(
|
|
160
|
+
data1: DataStructure,
|
|
161
|
+
data2: DataStructure,
|
|
162
|
+
ignore_paths: str | list[str] = "",
|
|
163
|
+
path_sep: str = "->",
|
|
164
|
+
comment_start: str = ">>",
|
|
165
|
+
comment_end: str = "<<",
|
|
166
|
+
) -> bool:
|
|
167
|
+
"""Compares two structures and returns `True` if they are equal and `False` otherwise.\n
|
|
168
|
+
⇾ Will not detect, if a key-name has changed, only if removed or added.\n
|
|
169
|
+
--------------------------------------------------------------------------------------------
|
|
170
|
+
Ignores the specified (found) key/s or item/s from `ignore_paths`. Comments are not ignored
|
|
171
|
+
when comparing. `comment_start` and `comment_end` are only used to correctly recognize the
|
|
172
|
+
keys in the `ignore_paths`.\n
|
|
173
|
+
--------------------------------------------------------------------------------------------
|
|
174
|
+
The paths from `ignore_paths` and the `path_sep` parameter work exactly the same way as for
|
|
175
|
+
the function `Data.get_path_id()`. See its documentation for more details."""
|
|
176
|
+
|
|
177
|
+
def process_ignore_paths(
|
|
178
|
+
ignore_paths: str | list[str],
|
|
179
|
+
) -> list[list[str]]:
|
|
180
|
+
if isinstance(ignore_paths, str):
|
|
181
|
+
ignore_paths = [ignore_paths]
|
|
182
|
+
return [path.split(path_sep) for path in ignore_paths if path]
|
|
183
|
+
|
|
184
|
+
def compare(
|
|
185
|
+
d1: DataStructure,
|
|
186
|
+
d2: DataStructure,
|
|
187
|
+
ignore_paths: list[list[str]],
|
|
188
|
+
current_path: list[str] = [],
|
|
189
|
+
) -> bool:
|
|
190
|
+
if any(current_path == path[: len(current_path)] for path in ignore_paths):
|
|
191
|
+
return True
|
|
192
|
+
if type(d1) != type(d2):
|
|
193
|
+
return False
|
|
194
|
+
if isinstance(d1, dict):
|
|
195
|
+
if set(d1.keys()) != set(d2.keys()):
|
|
196
|
+
return False
|
|
197
|
+
return all(compare(d1[key], d2[key], ignore_paths, current_path + [key]) for key in d1)
|
|
198
|
+
if isinstance(d1, (list, tuple)):
|
|
199
|
+
if len(d1) != len(d2):
|
|
200
|
+
return False
|
|
201
|
+
return all(
|
|
202
|
+
compare(item1, item2, ignore_paths, current_path + [str(i)])
|
|
203
|
+
for i, (item1, item2) in enumerate(zip(d1, d2))
|
|
204
|
+
)
|
|
205
|
+
if isinstance(d1, (set, frozenset)):
|
|
206
|
+
return d1 == d2
|
|
207
|
+
return d1 == d2
|
|
208
|
+
|
|
209
|
+
processed_data1 = Data.remove_comments(data1, comment_start, comment_end)
|
|
210
|
+
processed_data2 = Data.remove_comments(data2, comment_start, comment_end)
|
|
211
|
+
processed_ignore_paths = process_ignore_paths(ignore_paths)
|
|
212
|
+
return compare(processed_data1, processed_data2, processed_ignore_paths)
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def get_path_id(
|
|
216
|
+
data: DataStructure,
|
|
217
|
+
value_paths: str | list[str],
|
|
218
|
+
path_sep: str = "->",
|
|
219
|
+
comment_start: str = ">>",
|
|
220
|
+
comment_end: str = "<<",
|
|
221
|
+
ignore_not_found: bool = False,
|
|
222
|
+
) -> str | list[str]:
|
|
223
|
+
"""Generates a unique ID based on the path to a specific value within a nested data structure.\n
|
|
224
|
+
-------------------------------------------------------------------------------------------------
|
|
225
|
+
The `data` parameter is the list, tuple, or dictionary, which the id should be generated for.\n
|
|
226
|
+
-------------------------------------------------------------------------------------------------
|
|
227
|
+
The param `value_path` is a sort of path (or a list of paths) to the value/s to be updated.
|
|
228
|
+
In this example:
|
|
229
|
+
```python
|
|
230
|
+
{
|
|
231
|
+
"healthy": {
|
|
232
|
+
"fruit": ["apples", "bananas", "oranges"],
|
|
233
|
+
"vegetables": ["carrots", "broccoli", "celery"]
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
... if you want to change the value of `"apples"` to `"strawberries"`, the value path would be
|
|
238
|
+
`healthy->fruit->apples` or if you don't know that the value is `"apples"` you can also use the
|
|
239
|
+
index of the value, so `healthy->fruit->0`.\n
|
|
240
|
+
-------------------------------------------------------------------------------------------------
|
|
241
|
+
The comments marked with `comment_start` and `comment_end` will be removed, before trying to get
|
|
242
|
+
the path id.\n
|
|
243
|
+
-------------------------------------------------------------------------------------------------
|
|
244
|
+
The `path_sep` param is the separator between the keys/indexes in the path (default is `->` just
|
|
245
|
+
like in the example above).\n
|
|
246
|
+
-------------------------------------------------------------------------------------------------
|
|
247
|
+
If `ignore_not_found` is `True`, the function will return `None` if the value is not found
|
|
248
|
+
instead of raising an error."""
|
|
249
|
+
|
|
250
|
+
def process_path(path: str, data_obj: list | tuple | set | frozenset | dict) -> str | None:
|
|
251
|
+
keys = path.split(path_sep)
|
|
252
|
+
path_ids = []
|
|
253
|
+
max_id_length = 0
|
|
254
|
+
for key in keys:
|
|
255
|
+
if isinstance(data_obj, dict):
|
|
256
|
+
if key.isdigit():
|
|
257
|
+
if ignore_not_found:
|
|
258
|
+
return None
|
|
259
|
+
raise TypeError(f"Key '{key}' is invalid for a dict type.")
|
|
260
|
+
try:
|
|
261
|
+
idx = list(data_obj.keys()).index(key)
|
|
262
|
+
data_obj = data_obj[key]
|
|
263
|
+
except (ValueError, KeyError):
|
|
264
|
+
if ignore_not_found:
|
|
265
|
+
return None
|
|
266
|
+
raise KeyError(f"Key '{key}' not found in dict.")
|
|
267
|
+
elif isinstance(data_obj, (list, tuple, set, frozenset)):
|
|
268
|
+
try:
|
|
269
|
+
idx = int(key)
|
|
270
|
+
data_obj = list(data_obj)[idx] # CONVERT TO LIST FOR INDEXING
|
|
271
|
+
except ValueError:
|
|
272
|
+
try:
|
|
273
|
+
idx = list(data_obj).index(key)
|
|
274
|
+
data_obj = list(data_obj)[idx]
|
|
275
|
+
except ValueError:
|
|
276
|
+
if ignore_not_found:
|
|
277
|
+
return None
|
|
278
|
+
raise ValueError(f"Value '{key}' not found in '{type(data_obj).__name__}'")
|
|
279
|
+
else:
|
|
280
|
+
break
|
|
281
|
+
path_ids.append(str(idx))
|
|
282
|
+
max_id_length = max(max_id_length, len(str(idx)))
|
|
283
|
+
if not path_ids:
|
|
284
|
+
return None
|
|
285
|
+
return f"{max_id_length}>{''.join(id.zfill(max_id_length) for id in path_ids)}"
|
|
286
|
+
|
|
287
|
+
data = Data.remove_comments(data, comment_start, comment_end)
|
|
288
|
+
if isinstance(value_paths, str):
|
|
289
|
+
return process_path(value_paths, data)
|
|
290
|
+
results = [process_path(path, data) for path in value_paths]
|
|
291
|
+
return results if len(results) > 1 else results[0] if results else None
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) -> any:
|
|
295
|
+
"""Retrieves the value from `data` using the provided `path_id`.\n
|
|
296
|
+
-------------------------------------------------------------------------------------------------
|
|
297
|
+
Input your `data` along with a `path_id` that was created before using `Data.get_path_id()`.
|
|
298
|
+
If `get_key` is true and the final item is in a dict, it returns the key instead of the value.\n
|
|
299
|
+
-------------------------------------------------------------------------------------------------
|
|
300
|
+
The function will return the value (or key) from the path ID location, as long as the structure
|
|
301
|
+
of `data` hasn't changed since creating the path ID to that value."""
|
|
302
|
+
|
|
303
|
+
def get_nested(data: list | tuple | set | frozenset | dict, path: list[int], get_key: bool) -> any:
|
|
304
|
+
parent = None
|
|
305
|
+
for i, idx in enumerate(path):
|
|
306
|
+
if isinstance(data, dict):
|
|
307
|
+
keys = list(data.keys())
|
|
308
|
+
if i == len(path) - 1 and get_key:
|
|
309
|
+
return keys[idx]
|
|
310
|
+
parent = data
|
|
311
|
+
data = data[keys[idx]]
|
|
312
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
313
|
+
if i == len(path) - 1 and get_key:
|
|
314
|
+
if parent is None or not isinstance(parent, dict):
|
|
315
|
+
raise ValueError("Cannot get key from a non-dict parent")
|
|
316
|
+
return next(key for key, value in parent.items() if value is data)
|
|
317
|
+
parent = data
|
|
318
|
+
data = list(data)[idx] # CONVERT TO LIST FOR INDEXING
|
|
319
|
+
else:
|
|
320
|
+
raise TypeError(f"Unsupported type '{type(data)}' at path '{path[:i+1]}'")
|
|
321
|
+
return data
|
|
322
|
+
|
|
323
|
+
return get_nested(data, Data.__sep_path_id(path_id), get_key)
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def set_value_by_path_id(
|
|
327
|
+
data: DataStructure,
|
|
328
|
+
update_values: str | list[str],
|
|
329
|
+
sep: str = "::",
|
|
330
|
+
) -> list | tuple | dict:
|
|
331
|
+
"""Updates the value/s from `update_values` in the `data`.\n
|
|
332
|
+
--------------------------------------------------------------------------------
|
|
333
|
+
Input a list, tuple or dict as `data`, along with `update_values`, which is a
|
|
334
|
+
path ID that was created before using `Data.get_path_id()`, together with the
|
|
335
|
+
new value to be inserted where the path ID points to. The path ID and the new
|
|
336
|
+
value are separated by `sep`, which per default is `::`.\n
|
|
337
|
+
--------------------------------------------------------------------------------
|
|
338
|
+
The value from path ID will be changed to the new value, as long as the
|
|
339
|
+
structure of `data` hasn't changed since creating the path ID to that value."""
|
|
340
|
+
|
|
341
|
+
def update_nested(
|
|
342
|
+
data: list | tuple | set | frozenset | dict, path: list[int], value: any
|
|
343
|
+
) -> list | tuple | set | frozenset | dict:
|
|
344
|
+
if len(path) == 1:
|
|
345
|
+
if isinstance(data, dict):
|
|
346
|
+
keys = list(data.keys())
|
|
347
|
+
data = dict(data)
|
|
348
|
+
data[keys[path[0]]] = value
|
|
349
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
350
|
+
data = list(data)
|
|
351
|
+
data[path[0]] = value
|
|
352
|
+
data = type(data)(data)
|
|
353
|
+
else:
|
|
354
|
+
if isinstance(data, dict):
|
|
355
|
+
keys = list(data.keys())
|
|
356
|
+
key = keys[path[0]]
|
|
357
|
+
data = dict(data)
|
|
358
|
+
data[key] = update_nested(data[key], path[1:], value)
|
|
359
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
360
|
+
data = list(data)
|
|
361
|
+
data[path[0]] = update_nested(data[path[0]], path[1:], value)
|
|
362
|
+
data = type(data)(data)
|
|
363
|
+
return data
|
|
364
|
+
|
|
365
|
+
if isinstance(update_values, str):
|
|
366
|
+
update_values = [update_values]
|
|
367
|
+
valid_entries = [
|
|
368
|
+
(parts[0].strip(), parts[1])
|
|
369
|
+
for update_value in update_values
|
|
370
|
+
if len(parts := update_value.split(str(sep).strip())) == 2
|
|
371
|
+
]
|
|
372
|
+
if not valid_entries:
|
|
373
|
+
raise ValueError(f"No valid update_values found: {update_values}")
|
|
374
|
+
for path_id, new_val in valid_entries:
|
|
375
|
+
path = Data.__sep_path_id(path_id)
|
|
376
|
+
data = update_nested(data, path, new_val)
|
|
377
|
+
return data
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def print(
|
|
381
|
+
data: DataStructure,
|
|
382
|
+
indent: int = 4,
|
|
383
|
+
compactness: int = 1,
|
|
384
|
+
sep: str = ", ",
|
|
385
|
+
max_width: int = 127,
|
|
386
|
+
as_json: bool = False,
|
|
387
|
+
end: str = "\n",
|
|
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
|
+
@staticmethod
|
|
406
|
+
def to_str(
|
|
407
|
+
data: DataStructure,
|
|
408
|
+
indent: int = 4,
|
|
409
|
+
compactness: int = 1,
|
|
410
|
+
sep: str = ", ",
|
|
411
|
+
max_width: int = 127,
|
|
412
|
+
as_json: bool = False,
|
|
413
|
+
) -> str:
|
|
414
|
+
"""Get nicely formatted data structure-strings.\n
|
|
415
|
+
------------------------------------------------------------------------------
|
|
416
|
+
The indentation spaces-amount can be set with with `indent`.
|
|
417
|
+
There are three different levels of `compactness`:
|
|
418
|
+
- `0` expands everything possible
|
|
419
|
+
- `1` only expands if there's other lists, tuples or dicts inside of data or,
|
|
420
|
+
if the data's content is longer than `max_width`
|
|
421
|
+
- `2` keeps everything collapsed (all on one line)\n
|
|
422
|
+
------------------------------------------------------------------------------
|
|
423
|
+
If `as_json` is set to `True`, the output will be in valid JSON format."""
|
|
424
|
+
|
|
425
|
+
def format_value(value: any, current_indent: int) -> str:
|
|
426
|
+
if isinstance(value, dict):
|
|
427
|
+
return format_dict(value, current_indent + indent)
|
|
428
|
+
elif hasattr(value, "__dict__"):
|
|
429
|
+
return format_dict(value.__dict__, current_indent + indent)
|
|
430
|
+
elif isinstance(value, (list, tuple, set, frozenset)):
|
|
431
|
+
return format_sequence(value, current_indent + indent)
|
|
432
|
+
elif isinstance(value, bool):
|
|
433
|
+
return str(value).lower() if as_json else str(value)
|
|
434
|
+
elif isinstance(value, (int, float)):
|
|
435
|
+
return "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
|
|
436
|
+
elif isinstance(value, complex):
|
|
437
|
+
return f"[{value.real}, {value.imag}]" if as_json else str(value)
|
|
438
|
+
elif value is None:
|
|
439
|
+
return "null" if as_json else "None"
|
|
440
|
+
else:
|
|
441
|
+
return '"' + String.escape(str(value), '"') + '"' if as_json else "'" + String.escape(str(value), "'") + "'"
|
|
442
|
+
|
|
443
|
+
def should_expand(seq: list | tuple | dict) -> bool:
|
|
444
|
+
if compactness == 0:
|
|
445
|
+
return True
|
|
446
|
+
if compactness == 2:
|
|
447
|
+
return False
|
|
448
|
+
complex_items = sum(1 for item in seq if isinstance(item, (list, tuple, dict, set, frozenset)))
|
|
449
|
+
return (
|
|
450
|
+
complex_items > 1
|
|
451
|
+
or (complex_items == 1 and len(seq) > 1)
|
|
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))
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
def format_dict(d: dict, current_indent: int) -> str:
|
|
463
|
+
if not d or compactness == 2:
|
|
464
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
465
|
+
if not should_expand(d.values()):
|
|
466
|
+
return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
|
|
467
|
+
items = []
|
|
468
|
+
for key, value in d.items():
|
|
469
|
+
formatted_value = format_value(value, current_indent)
|
|
470
|
+
items.append(f'{" " * (current_indent + indent)}{format_key(key)}: {formatted_value}')
|
|
471
|
+
return "{\n" + ",\n".join(items) + f'\n{" " * current_indent}}}'
|
|
472
|
+
|
|
473
|
+
def format_sequence(seq, current_indent: int) -> str:
|
|
474
|
+
if as_json:
|
|
475
|
+
seq = list(seq)
|
|
476
|
+
if not seq or compactness == 2:
|
|
477
|
+
return (
|
|
478
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
479
|
+
if isinstance(seq, list)
|
|
480
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
481
|
+
)
|
|
482
|
+
if not should_expand(seq):
|
|
483
|
+
return (
|
|
484
|
+
"[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
|
|
485
|
+
if isinstance(seq, list)
|
|
486
|
+
else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
|
|
487
|
+
)
|
|
488
|
+
items = [format_value(item, current_indent) for item in seq]
|
|
489
|
+
formatted_items = ",\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
|
|
490
|
+
if isinstance(seq, list):
|
|
491
|
+
return "[\n" + formatted_items + f'\n{" " * current_indent}]'
|
|
492
|
+
else:
|
|
493
|
+
return "(\n" + formatted_items + f'\n{" " * current_indent})'
|
|
494
|
+
|
|
495
|
+
return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def _is_key(data: DataStructure, path_id: str) -> bool:
|
|
499
|
+
"""Returns `True` if the path ID points to a key in `data` and `False` otherwise.\n
|
|
500
|
+
------------------------------------------------------------------------------------
|
|
501
|
+
Input a list, tuple or dict as `data`, along with `path_id`, which is a path ID
|
|
502
|
+
that was created before using `Data.get_path_id()`."""
|
|
503
|
+
|
|
504
|
+
def check_nested(data: list | tuple | set | frozenset | dict, path: list[int]) -> bool:
|
|
505
|
+
for i, idx in enumerate(path):
|
|
506
|
+
if isinstance(data, dict):
|
|
507
|
+
keys = list(data.keys())
|
|
508
|
+
if i == len(path) - 1:
|
|
509
|
+
return True
|
|
510
|
+
try:
|
|
511
|
+
data = data[keys[idx]]
|
|
512
|
+
except IndexError:
|
|
513
|
+
return False
|
|
514
|
+
elif isinstance(data, (list, tuple, set, frozenset)):
|
|
515
|
+
return False
|
|
516
|
+
else:
|
|
517
|
+
raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
if not isinstance(data, dict):
|
|
521
|
+
return False
|
|
522
|
+
path = Data.__sep_path_id(path_id)
|
|
523
|
+
return check_nested(data, path)
|
|
524
|
+
|
|
525
|
+
@staticmethod
|
|
526
|
+
def __sep_path_id(path_id: str) -> list[int]:
|
|
527
|
+
if path_id.count(">") != 1:
|
|
528
|
+
raise ValueError(f"Invalid path ID: {path_id}")
|
|
529
|
+
id_part_len = int(path_id.split(">")[0])
|
|
530
|
+
path_ids_str = path_id.split(">")[1]
|
|
531
|
+
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
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
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()`
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .xx_path import Path
|
|
10
|
+
|
|
11
|
+
import os as _os
|
|
12
|
+
import sys as _sys
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EnvPath:
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def paths(as_list: bool = False) -> str | list:
|
|
19
|
+
"""Get the PATH environment variable."""
|
|
20
|
+
paths = _os.environ.get("PATH", "")
|
|
21
|
+
return paths.split(_os.pathsep) if as_list else paths
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def has_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> bool:
|
|
25
|
+
"""Check if a path is present in the PATH environment variable."""
|
|
26
|
+
if cwd:
|
|
27
|
+
path = _os.getcwd()
|
|
28
|
+
elif base_dir:
|
|
29
|
+
path = Path.get(base_dir=True)
|
|
30
|
+
elif path is None:
|
|
31
|
+
raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
|
|
32
|
+
paths = EnvPath.paths(as_list=True)
|
|
33
|
+
return _os.path.normpath(path) in [_os.path.normpath(p) for p in paths]
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def add_path(
|
|
37
|
+
path: str = None,
|
|
38
|
+
cwd: bool = False,
|
|
39
|
+
base_dir: bool = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Add a path to the PATH environment variable."""
|
|
42
|
+
path = EnvPath.__get(path, cwd, base_dir)
|
|
43
|
+
if not EnvPath.has_path(path):
|
|
44
|
+
EnvPath.__persistent(path, add=True)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def remove_path(
|
|
48
|
+
path: str = None,
|
|
49
|
+
cwd: bool = False,
|
|
50
|
+
base_dir: bool = False,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Remove a path from the PATH environment variable."""
|
|
53
|
+
path = EnvPath.__get(path, cwd, base_dir)
|
|
54
|
+
if EnvPath.has_path(path):
|
|
55
|
+
EnvPath.__persistent(path, remove=True)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def __get(
|
|
59
|
+
path: str = None,
|
|
60
|
+
cwd: bool = False,
|
|
61
|
+
base_dir: bool = False,
|
|
62
|
+
) -> list:
|
|
63
|
+
"""Get and/or normalize the paths.\n
|
|
64
|
+
------------------------------------------------------------------------------------
|
|
65
|
+
Raise an error if no path is provided and neither `cwd` or `base_dir` is `True`."""
|
|
66
|
+
if cwd:
|
|
67
|
+
path = _os.getcwd()
|
|
68
|
+
elif base_dir:
|
|
69
|
+
path = Path.get(base_dir=True)
|
|
70
|
+
elif path is None:
|
|
71
|
+
raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
|
|
72
|
+
return _os.path.normpath(path)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def __persistent(path: str, add: bool = False, remove: bool = False) -> None:
|
|
76
|
+
"""Add or remove a path from PATH persistently across sessions as well as the current session."""
|
|
77
|
+
if add == remove:
|
|
78
|
+
raise ValueError("Either add or remove must be True, but not both.")
|
|
79
|
+
current_paths = EnvPath.paths(as_list=True)
|
|
80
|
+
path = _os.path.normpath(path)
|
|
81
|
+
if remove:
|
|
82
|
+
current_paths = [p for p in current_paths if _os.path.normpath(p) != _os.path.normpath(path)]
|
|
83
|
+
elif add:
|
|
84
|
+
current_paths.append(path)
|
|
85
|
+
_os.environ["PATH"] = new_path = _os.pathsep.join(sorted(set(filter(bool, current_paths))))
|
|
86
|
+
if _sys.platform == "win32": # Windows
|
|
87
|
+
try:
|
|
88
|
+
import winreg as _winreg
|
|
89
|
+
|
|
90
|
+
key = _winreg.OpenKey(
|
|
91
|
+
_winreg.HKEY_CURRENT_USER,
|
|
92
|
+
"Environment",
|
|
93
|
+
0,
|
|
94
|
+
_winreg.KEY_ALL_ACCESS,
|
|
95
|
+
)
|
|
96
|
+
_winreg.SetValueEx(key, "PATH", 0, _winreg.REG_EXPAND_SZ, new_path)
|
|
97
|
+
_winreg.CloseKey(key)
|
|
98
|
+
except ImportError:
|
|
99
|
+
print("Warning: Unable to make persistent changes on Windows.")
|
|
100
|
+
else: # UNIX-like (Linux/macOS)
|
|
101
|
+
shell_rc_file = _os.path.expanduser(
|
|
102
|
+
"~/.bashrc" if _os.path.exists(_os.path.expanduser("~/.bashrc")) else "~/.zshrc"
|
|
103
|
+
)
|
|
104
|
+
with open(shell_rc_file, "r+") as f:
|
|
105
|
+
content = f.read()
|
|
106
|
+
f.seek(0)
|
|
107
|
+
if remove:
|
|
108
|
+
new_content = [line for line in content.splitlines() if not line.endswith(f':{path}"')]
|
|
109
|
+
f.write("\n".join(new_content))
|
|
110
|
+
else:
|
|
111
|
+
f.write(f'{content.rstrip()}\n# Added by XulbuX\nexport PATH="{new_path}"\n')
|
|
112
|
+
f.truncate()
|
|
113
|
+
_os.system(f"source {shell_rc_file}")
|