xulbux 1.5.7__py3-none-any.whl → 1.5.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/xx_data.py CHANGED
@@ -1,115 +1,121 @@
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]
9
+
10
+
5
11
  class Data:
6
12
 
7
13
  @staticmethod
8
- def chars_count(data: list | tuple | set | frozenset | dict) -> int:
9
- """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."""
10
16
  if isinstance(data, dict):
11
17
  return sum(len(str(k)) + len(str(v)) for k, v in data.items())
12
18
  return sum(len(str(item)) for item in data)
13
19
 
14
20
  @staticmethod
15
- 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."""
16
23
  if isinstance(data, dict):
17
- return {k: v.strip() if isinstance(v, str) else Data.strip(v) for k, v in data.items()}
18
- elif isinstance(data, (list, tuple)):
19
- stripped = [item.strip() if isinstance(item, str) else Data.strip(item) for item in data]
20
- return tuple(stripped) if isinstance(data, tuple) else stripped
21
- return data.strip() if isinstance(data, str) else data
24
+ return {k: Data.strip(v) for k, v in data.items()}
25
+ return type(data)(map(Data.strip, data))
22
26
 
23
27
  @staticmethod
24
- def remove(data: list | tuple | dict, items: list[str]) -> list | tuple | dict:
25
- """Remove multiple items from lists and tuples or keys from dictionaries."""
26
- if isinstance(data, (list, tuple)):
27
- result = [k for k in data if k not in items]
28
- return result if isinstance(data, list) else tuple(result)
29
- elif isinstance(data, dict):
30
- return {k: v for k, v in data.items() if k not in items}
31
-
32
- @staticmethod
33
- 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."""
34
31
  if isinstance(data, dict):
35
- filtered_dict = {}
36
- for key, value in data.items():
37
- if isinstance(value, (list, tuple, dict)):
38
- filtered_value = Data.remove_empty_items(value, spaces_are_empty)
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
- ):
44
- filtered_dict[key] = value
45
- return filtered_dict
46
- filtered = []
47
- for item in data:
48
- if isinstance(item, (list, tuple, dict)):
49
- deduped_item = Data.remove_empty_items(item, spaces_are_empty)
50
- if deduped_item:
51
- if isinstance(item, tuple):
52
- deduped_item = tuple(deduped_item)
53
- filtered.append(deduped_item)
54
- elif item not in (None, "") and not ((spaces_are_empty and isinstance(item, str)) and item.strip() in (None, "")):
55
- filtered.append(item)
56
- 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
57
56
 
58
57
  @staticmethod
59
- 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."""
60
60
  if isinstance(data, dict):
61
61
  return {k: Data.remove_duplicates(v) for k, v in data.items()}
62
- elif isinstance(data, (list, tuple)):
63
- unique_items = []
64
- for item in data:
65
- if isinstance(item, (list, tuple, set, dict)):
66
- deduped_item = Data.remove_duplicates(item)
67
- if deduped_item not in unique_items:
68
- unique_items.append(deduped_item)
69
- elif item not in unique_items:
70
- unique_items.append(item)
71
- 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
+ )
72
72
  return data
73
73
 
74
74
  @staticmethod
75
75
  def remove_comments(
76
- data: list | tuple | dict,
76
+ data: DataStructure,
77
77
  comment_start: str = ">>",
78
78
  comment_end: str = "<<",
79
79
  comment_sep: str = "",
80
- ) -> list | tuple | dict:
80
+ ) -> DataStructure:
81
81
  """Remove comments from a list, tuple or dictionary.\n
82
- -----------------------------------------------------------------------------------------------------------------
82
+ --------------------------------------------------------------------------------------------------------------------
83
83
  The `data` parameter is your list, tuple or dictionary, where the comments should get removed from.<br>
84
84
  The `comment_start` parameter is the string that marks the start of a comment inside `data`. (default: `>>`)<br>
85
85
  The `comment_end` parameter is the string that marks the end of a comment inside `data`. (default: `<<`)<br>
86
- The `comment_sep` parameter is a string with which a comment will be replaced, if it is between strings.\n
87
- -----------------------------------------------------------------------------------------------------------------
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
88
  Examples:\n
89
89
  ```python\n data = {
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'
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"
95
95
  ],
96
- '>> FULL KEY + ALL ITS VALUES ARE A COMMENT key2': [
97
- 'value',
98
- 'value',
99
- 'value'
96
+ ">> FULL KEY + ALL ITS VALUES ARE A COMMENT key2": [
97
+ "value",
98
+ "value",
99
+ "value"
100
100
  ],
101
- 'key3': '>> ALL THE KEYS VALUES ARE COMMENTS value'
101
+ "key3": ">> ALL THE KEYS VALUES ARE COMMENTS value"
102
102
  }
103
- processed_data = Data.remove_comments(data, comment_start='>>', comment_end='<<', comment_sep='__')\n```
104
- -----------------------------------------------------------------------------------------------------------------
103
+
104
+ processed_data = Data.remove_comments(
105
+ data,
106
+ comment_start=">>",
107
+ comment_end="<<",
108
+ comment_sep="__"
109
+ )\n```
110
+ --------------------------------------------------------------------------------------------------------------------
105
111
  For this example, `processed_data` will be:
106
112
  ```python\n {
107
- 'key1': [
108
- 'value1',
109
- 'value2',
110
- 'val__ue3'
113
+ "key1": [
114
+ "value1",
115
+ "value2",
116
+ "val__ue3"
111
117
  ],
112
- 'key3': None
118
+ "key3": None
113
119
  }\n```
114
120
  For `key1`, all the comments will just be removed, except at `value3` and `value4`:<br>
115
121
   `value3` The comment is removed and the parts left and right are joined through `comment_sep`.<br>
@@ -118,114 +124,98 @@ class Data:
118
124
  For `key3`, since all its values are just comments, the key will still exist, but with a value of `None`.
119
125
  """
120
126
 
121
- def process_item(
122
- item: dict | list | tuple | str,
123
- ) -> dict | list | tuple | str | None:
124
- if isinstance(item, dict):
125
- processed_dict = {}
126
- for key, val in item.items():
127
- processed_key = process_item(key)
128
- if processed_key is not None:
129
- processed_val = process_item(val)
130
- if isinstance(val, (list, tuple, dict)):
131
- if processed_val:
132
- processed_dict[processed_key] = processed_val
133
- elif processed_val is not None:
134
- processed_dict[processed_key] = processed_val
135
- else:
136
- processed_dict[processed_key] = None
137
- return processed_dict
138
- elif isinstance(item, list):
139
- return [v for v in (process_item(val) for val in item) if v is not None]
140
- elif isinstance(item, tuple):
141
- return tuple(v for v in (process_item(val) for val in item) if v is not None)
142
- elif isinstance(item, str):
143
- if comment_end:
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
- )
149
- else:
150
- no_comments = None if item.lstrip().startswith(comment_start) else item
151
- return no_comments.strip() if no_comments and no_comments.strip() != "" else None
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
152
139
  else:
153
- 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
154
153
 
155
154
  return process_item(data)
156
155
 
157
156
  @staticmethod
158
157
  def is_equal(
159
- data1: list | tuple | dict,
160
- data2: list | tuple | dict,
158
+ data1: DataStructure,
159
+ data2: DataStructure,
161
160
  ignore_paths: str | list[str] = "",
161
+ path_sep: str = "->",
162
162
  comment_start: str = ">>",
163
163
  comment_end: str = "<<",
164
- sep: str = "->",
165
164
  ) -> bool:
166
165
  """Compares two structures and returns `True` if they are equal and `False` otherwise.\n
167
166
  ⇾ **Will not detect, if a key-name has changed, only if removed or added.**\n
168
167
  ------------------------------------------------------------------------------------------------
169
168
  Ignores the specified (found) key/s or item/s from `ignore_paths`. Comments are not ignored<br>
170
- 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
171
171
  ------------------------------------------------------------------------------------------------
172
- The paths from `ignore_paths` work exactly the same way as the paths from `value_paths`<br>
173
- in the function `Data.get_path_id()`, just like the `sep` parameter. For more detailed<br>
174
- explanation, see the documentation of the function `Data.get_path_id()`.
175
- """
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."""
176
174
 
177
175
  def process_ignore_paths(
178
176
  ignore_paths: str | list[str],
179
177
  ) -> list[list[str]]:
180
178
  if isinstance(ignore_paths, str):
181
179
  ignore_paths = [ignore_paths]
182
- return [path.split(sep) for path in ignore_paths if path]
180
+ return [path.split(path_sep) for path in ignore_paths if path]
183
181
 
184
182
  def compare(
185
- d1: dict | list | tuple,
186
- d2: dict | list | tuple,
183
+ d1: DataStructure,
184
+ d2: DataStructure,
187
185
  ignore_paths: list[list[str]],
188
- current_path: list = [],
186
+ current_path: list[str] = [],
189
187
  ) -> 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
- ):
188
+ if any(current_path == path[: len(current_path)] for path in ignore_paths):
193
189
  return True
194
- if isinstance(d1, dict) and isinstance(d2, dict):
190
+ if type(d1) != type(d2):
191
+ return False
192
+ if isinstance(d1, dict):
195
193
  if set(d1.keys()) != set(d2.keys()):
196
194
  return False
197
195
  return all(compare(d1[key], d2[key], ignore_paths, current_path + [key]) for key in d1)
198
- elif isinstance(d1, (list, tuple)) and isinstance(d2, (list, tuple)):
196
+ if isinstance(d1, (list, tuple)):
199
197
  if len(d1) != len(d2):
200
198
  return False
201
199
  return all(
202
200
  compare(item1, item2, ignore_paths, current_path + [str(i)])
203
201
  for i, (item1, item2) in enumerate(zip(d1, d2))
204
202
  )
205
- else:
203
+ if isinstance(d1, (set, frozenset)):
206
204
  return d1 == d2
205
+ return d1 == d2
207
206
 
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
- )
213
-
214
- @staticmethod
215
- def get_fingerprint(
216
- data: list | tuple | dict,
217
- ) -> list | tuple | dict | None:
218
- if isinstance(data, dict):
219
- return {i: type(v).__name__ for i, v in enumerate(data.values())}
220
- elif isinstance(data, (list, tuple)):
221
- return {i: type(v).__name__ for i, v in enumerate(data)}
222
- 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)
223
211
 
224
212
  @staticmethod
225
213
  def get_path_id(
226
- data: list | tuple | dict,
214
+ data: DataStructure,
227
215
  value_paths: str | list[str],
228
- sep: str = "->",
216
+ path_sep: str = "->",
217
+ comment_start: str = ">>",
218
+ comment_end: str = "<<",
229
219
  ignore_not_found: bool = False,
230
220
  ) -> str | list[str]:
231
221
  """Generates a unique ID based on the path to a specific value within a nested data structure.\n
@@ -235,83 +225,78 @@ class Data:
235
225
  The param `value_path` is a sort of path (or a list of paths) to the value/s to be updated.<br>
236
226
  In this example:
237
227
  ```\n {
238
- 'healthy': {
239
- 'fruit': ['apples', 'bananas', 'oranges'],
240
- 'vegetables': ['carrots', 'broccoli', 'celery']
241
- }
228
+ "healthy": {
229
+ "fruit": ["apples", "bananas", "oranges"],
230
+ "vegetables": ["carrots", "broccoli", "celery"]
231
+ }
242
232
  }\n```
243
- ... if you want to change the value of `'apples'` to `'strawberries'`, `value_path`<br>
244
- would be `healthy->fruit->apples` or if you don't know that the value is `apples`<br>
245
- 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
246
236
  -------------------------------------------------------------------------------------------------
247
- The `sep` param is the separator between the keys in the path<br>
237
+ The comments marked with `comment_start` and `comment_end` will be removed,<br>
238
+ before trying to get the path id.\n
239
+ -------------------------------------------------------------------------------------------------
240
+ The `path_sep` param is the separator between the keys/indexes in the path<br>
248
241
  (default is `->` just like in the example above).\n
249
242
  -------------------------------------------------------------------------------------------------
250
243
  If `ignore_not_found` is `True`, the function will return `None` if the value is not<br>
251
244
  found instead of raising an error."""
252
- if isinstance(value_paths, str):
253
- value_paths = [value_paths]
254
- path_ids = []
255
- for path in value_paths:
256
- keys = [k.strip() for k in path.split(str(sep).strip()) if k.strip() != ""]
257
- id_part_len, _path_ids, _obj = 0, [], data
258
- try:
259
- for k in keys:
260
- if isinstance(_obj, dict):
261
- if k.isdigit():
262
- raise TypeError(f"Key '{k}' is invalid for a dict type.")
263
- try:
264
- idx = list(_obj.keys()).index(k)
265
- _path_ids.append(idx)
266
- _obj = _obj[k]
267
- except KeyError:
268
- if ignore_not_found:
269
- _path_ids = None
270
- break
271
- raise KeyError(f"Key '{k}' not found in dict.")
272
- 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:
273
268
  try:
274
- idx = int(k)
275
- _path_ids.append(idx)
276
- _obj = _obj[idx]
269
+ idx = list(data_obj).index(key)
270
+ data_obj = list(data_obj)[idx]
277
271
  except ValueError:
278
- try:
279
- idx = _obj.index(k)
280
- _path_ids.append(idx)
281
- _obj = _obj[idx]
282
- except ValueError:
283
- if ignore_not_found:
284
- _path_ids = None
285
- break
286
- raise ValueError(f"Value '{k}' not found in list/tuple.")
287
- else:
288
- break
289
- if _path_ids:
290
- id_part_len = max(id_part_len, len(str(_path_ids[-1])))
291
- if _path_ids is not None:
292
- path_ids.append(f'{id_part_len}>{"".join([str(id).zfill(id_part_len) for id in _path_ids])}')
293
- elif ignore_not_found:
294
- path_ids.append(None)
295
- except (KeyError, ValueError, TypeError) as e:
296
- if ignore_not_found:
297
- path_ids.append(None)
272
+ if ignore_not_found:
273
+ return None
274
+ raise ValueError(f"Value '{key}' not found in '{type(data_obj).__name__}'")
298
275
  else:
299
- raise e
300
- return path_ids if len(path_ids) > 1 else path_ids[0] if len(path_ids) == 1 else None
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
301
288
 
302
289
  @staticmethod
303
- 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:
304
291
  """Retrieves the value from `data` using the provided `path_id`.\n
305
- ------------------------------------------------------------------------------------
306
- Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
307
- that was created before using `Object.get_path_id()`. If `get_key` is True<br>
308
- and the final item is in a dict, it returns the key instead of the value.\n
309
- ------------------------------------------------------------------------------------
310
- The function will return the value (or key) from the path-id location, as long as<br>
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:
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:
315
300
  parent = None
316
301
  for i, idx in enumerate(path):
317
302
  if isinstance(data, dict):
@@ -320,54 +305,57 @@ class Data:
320
305
  return keys[idx]
321
306
  parent = data
322
307
  data = data[keys[idx]]
323
- elif isinstance(data, (list, tuple)):
308
+ elif isinstance(data, (list, tuple, set, frozenset)):
324
309
  if i == len(path) - 1 and get_key:
325
310
  if parent is None or not isinstance(parent, dict):
326
- raise ValueError("Cannot get key from list or tuple without a parent dictionary")
311
+ raise ValueError("Cannot get key from a non-dict parent")
327
312
  return next(key for key, value in parent.items() if value is data)
328
313
  parent = data
329
- data = data[idx]
314
+ data = list(data)[idx] # CONVERT TO LIST FOR INDEXING
330
315
  else:
331
- 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]}'")
332
317
  return data
333
318
 
334
- path = Data._sep_path_id(path_id)
335
- return get_nested(data, path, get_key)
319
+ return get_nested(data, Data.__sep_path_id(path_id), get_key)
336
320
 
337
321
  @staticmethod
338
322
  def set_value_by_path_id(
339
- data: list | tuple | dict,
323
+ data: DataStructure,
340
324
  update_values: str | list[str],
341
325
  sep: str = "::",
342
326
  ) -> list | tuple | dict:
343
327
  """Updates the value/s from `update_values` in the `data`.\n
344
328
  --------------------------------------------------------------------------------
345
329
  Input a list, tuple or dict as `data`, along with `update_values`, which is<br>
346
- a path-id that was created before using `Object.get_path_id()`, together<br>
347
- 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>
348
332
  and the new value are separated by `sep`, which per default is `::`.\n
349
333
  --------------------------------------------------------------------------------
350
- The value from path-id will be changed to the new value, as long as the<br>
351
- structure of `data` hasn't changed since creating the path-id to that value.
352
- """
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."""
353
336
 
354
- def update_nested(data: list | tuple | dict, path: list[int], value: any) -> list | tuple | dict:
337
+ def update_nested(
338
+ data: list | tuple | set | frozenset | dict, path: list[int], value: any
339
+ ) -> list | tuple | set | frozenset | dict:
355
340
  if len(path) == 1:
356
341
  if isinstance(data, dict):
357
342
  keys = list(data.keys())
343
+ data = dict(data)
358
344
  data[keys[path[0]]] = value
359
- elif isinstance(data, (list, tuple)):
345
+ elif isinstance(data, (list, tuple, set, frozenset)):
360
346
  data = list(data)
361
347
  data[path[0]] = value
362
348
  data = type(data)(data)
363
- elif isinstance(data, dict):
364
- keys = list(data.keys())
365
- key = keys[path[0]]
366
- data[key] = update_nested(data[key], path[1:], value)
367
- elif isinstance(data, (list, tuple)):
368
- data = list(data)
369
- data[path[0]] = update_nested(data[path[0]], path[1:], value)
370
- 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)
371
359
  return data
372
360
 
373
361
  if isinstance(update_values, str):
@@ -379,19 +367,18 @@ class Data:
379
367
  ]
380
368
  if not valid_entries:
381
369
  raise ValueError(f"No valid update_values found: {update_values}")
382
- path, new_values = zip(*valid_entries) if valid_entries else ([], [])
383
- for path_id, new_val in zip(path, new_values):
384
- path = Data._sep_path_id(path_id)
370
+ for path_id, new_val in valid_entries:
371
+ path = Data.__sep_path_id(path_id)
385
372
  data = update_nested(data, path, new_val)
386
373
  return data
387
374
 
388
375
  @staticmethod
389
376
  def print(
390
- data: list | tuple | dict,
391
- indent: int = 2,
377
+ data: DataStructure,
378
+ indent: int = 4,
392
379
  compactness: int = 1,
393
380
  sep: str = ", ",
394
- max_width: int = 140,
381
+ max_width: int = 127,
395
382
  as_json: bool = False,
396
383
  end: str = "\n",
397
384
  ) -> None:
@@ -414,11 +401,11 @@ class Data:
414
401
 
415
402
  @staticmethod
416
403
  def to_str(
417
- data: list | tuple | dict,
418
- indent: int = 2,
404
+ data: DataStructure,
405
+ indent: int = 4,
419
406
  compactness: int = 1,
420
407
  sep: str = ", ",
421
- max_width: int = 140,
408
+ max_width: int = 127,
422
409
  as_json: bool = False,
423
410
  ) -> str:
424
411
  """Get nicely formatted data structure-strings.\n
@@ -430,24 +417,7 @@ class Data:
430
417
   ⠀if the data's content is longer than `max_width`<br>
431
418
  `2` keeps everything collapsed (all on one line)\n
432
419
  ------------------------------------------------------------------------------------
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"\'")
450
- return s
420
+ If `as_json` is set to `True`, the output will be in valid JSON format."""
451
421
 
452
422
  def format_value(value: any, current_indent: int) -> str:
453
423
  if isinstance(value, dict):
@@ -465,7 +435,7 @@ class Data:
465
435
  elif value is None:
466
436
  return "null" if as_json else "None"
467
437
  else:
468
- return '"' + escape_string(str(value), '"') + '"' if as_json else "'" + escape_string(str(value), "'") + "'"
438
+ return '"' + String.escape(str(value), '"') + '"' if as_json else "'" + String.escape(str(value), "'") + "'"
469
439
 
470
440
  def should_expand(seq: list | tuple | dict) -> bool:
471
441
  if compactness == 0:
@@ -481,9 +451,9 @@ class Data:
481
451
 
482
452
  def format_key(k: any) -> str:
483
453
  return (
484
- '"' + escape_string(str(k), '"') + '"'
454
+ '"' + String.escape(str(k), '"') + '"'
485
455
  if as_json
486
- else ("'" + escape_string(str(k), "'") + "'" if isinstance(k, str) else str(k))
456
+ else ("'" + String.escape(str(k), "'") + "'" if isinstance(k, str) else str(k))
487
457
  )
488
458
 
489
459
  def format_dict(d: dict, current_indent: int) -> str:
@@ -522,34 +492,37 @@ class Data:
522
492
  return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
523
493
 
524
494
  @staticmethod
525
- def _is_key(data: list | tuple | dict, path_id: str) -> bool:
526
- """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
527
497
  ------------------------------------------------------------------------------------
528
- Input a list, tuple or dict as `data`, along with `path_id`, which is a path-id<br>
529
- that was created before using `Object.get_path_id()`."""
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()`."""
530
500
 
531
- def check_nested(data: list | tuple | dict, path: list[int]) -> bool:
501
+ def check_nested(data: list | tuple | set | frozenset | dict, path: list[int]) -> bool:
532
502
  for i, idx in enumerate(path):
533
503
  if isinstance(data, dict):
534
504
  keys = list(data.keys())
535
505
  if i == len(path) - 1:
536
506
  return True
537
- data = data[keys[idx]]
538
- 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)):
539
512
  return False
540
513
  else:
541
514
  raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
542
515
  return False
543
516
 
544
- if isinstance(data, (list, tuple)):
517
+ if not isinstance(data, dict):
545
518
  return False
546
- path = Data._sep_path_id(path_id)
519
+ path = Data.__sep_path_id(path_id)
547
520
  return check_nested(data, path)
548
521
 
549
522
  @staticmethod
550
- def _sep_path_id(path_id: str) -> list[int]:
523
+ def __sep_path_id(path_id: str) -> list[int]:
551
524
  if path_id.count(">") != 1:
552
- raise ValueError(f"Invalid path-id: {path_id}")
525
+ raise ValueError(f"Invalid path ID: {path_id}")
553
526
  id_part_len = int(path_id.split(">")[0])
554
527
  path_ids_str = path_id.split(">")[1]
555
528
  return [int(path_ids_str[i : i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]