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