xulbux 1.6.7__py3-none-any.whl → 1.6.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_console.py CHANGED
@@ -11,7 +11,7 @@ from .xx_string import String
11
11
  from .xx_color import Color, rgba, hexa
12
12
 
13
13
  from prompt_toolkit.key_binding.key_bindings import KeyBindings
14
- from typing import Optional
14
+ from typing import Optional, Any
15
15
  import prompt_toolkit as _prompt_toolkit
16
16
  import pyperclip as _pyperclip
17
17
  import keyboard as _keyboard
@@ -22,39 +22,83 @@ import sys as _sys
22
22
  import os as _os
23
23
 
24
24
 
25
- # YAPF: disable
26
25
  class _ConsoleWidth:
26
+
27
27
  def __get__(self, obj, owner=None):
28
28
  return _os.get_terminal_size().columns
29
29
 
30
+
30
31
  class _ConsoleHeight:
32
+
31
33
  def __get__(self, obj, owner=None):
32
34
  return _os.get_terminal_size().lines
33
35
 
36
+
34
37
  class _ConsoleSize:
38
+
35
39
  def __get__(self, obj, owner=None):
36
40
  size = _os.get_terminal_size()
37
41
  return (size.columns, size.lines)
38
42
 
43
+
39
44
  class _ConsoleUser:
45
+
40
46
  def __get__(self, obj, owner=None):
41
47
  return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
42
48
 
49
+
43
50
  class ArgResult:
44
51
  """Exists: if the argument was found or not\n
45
52
  Value: the value from behind the found argument"""
46
- def __init__(self, exists: bool, value: any):
53
+
54
+ def __init__(self, exists: bool, value: Any):
47
55
  self.exists = exists
48
56
  self.value = value
49
57
 
58
+ def __bool__(self):
59
+ return self.exists
60
+
61
+
50
62
  class Args:
51
- """Stores arguments under their aliases with their results."""
63
+ """Stores found command arguments under their aliases with their results."""
64
+
52
65
  def __init__(self, **kwargs):
53
66
  for key, value in kwargs.items():
54
67
  if not key.isidentifier():
55
68
  raise TypeError(f"Argument alias '{key}' is invalid. It must be a valid Python variable name.")
56
69
  setattr(self, key, ArgResult(**value))
57
- # YAPF: enable
70
+
71
+ def __len__(self):
72
+ return len(vars(self))
73
+
74
+ def __contains__(self, key):
75
+ return hasattr(self, key)
76
+
77
+ def __getitem__(self, key):
78
+ if isinstance(key, int):
79
+ return list(self.__iter__())[key]
80
+ return getattr(self, key)
81
+
82
+ def __iter__(self):
83
+ for key, value in vars(self).items():
84
+ yield (key, {"exists": value.exists, "value": value.value})
85
+
86
+ def dict(self) -> dict[str, dict[str, Any]]:
87
+ """Returns the arguments as a dictionary."""
88
+ return {k: {"exists": v.exists, "value": v.value} for k, v in vars(self).items()}
89
+
90
+ def keys(self):
91
+ """Returns the argument aliases as `dict_keys([...])`."""
92
+ return vars(self).keys()
93
+
94
+ def values(self):
95
+ """Returns the argument results as `dict_values([...])`."""
96
+ return vars(self).values()
97
+
98
+ def items(self):
99
+ """Yields tuples of `(alias, {'exists': bool, 'value': Any})`."""
100
+ for key, value in self.__iter__():
101
+ yield (key, value)
58
102
 
59
103
 
60
104
  class Console:
@@ -70,46 +114,105 @@ class Console:
70
114
  """The name of the current user."""
71
115
 
72
116
  @staticmethod
73
- def get_args(find_args: dict[str, list[str] | tuple[str, ...]]) -> Args:
117
+ def get_args(
118
+ find_args: dict[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]],
119
+ allow_spaces: bool = False
120
+ ) -> Args:
74
121
  """Will search for the specified arguments in the command line
75
122
  arguments and return the results as a special `Args` object.\n
76
123
  ----------------------------------------------------------------
77
- The `find_args` dictionary should have the following structure:
124
+ The `find_args` dictionary can have the following structures for each alias:
125
+ 1. Simple list/tuple of flags (when no default value is needed):
126
+ ```python
127
+ "alias_name": ["-f", "--flag"]
128
+ ```
129
+ 2. Dictionary with 'flags' and optional 'default':
130
+ ```python
131
+ "alias_name": {
132
+ "flags": ["-f", "--flag"],
133
+ "default": "some_value" # Optional
134
+ }
135
+ ```
136
+ Example `find_args`:
78
137
  ```python
79
138
  find_args={
80
- "arg1_alias": ["-a1", "--arg1", "--argument-1"],
81
- "arg2_alias": ("-a2", "--arg2", "--argument-2"),
82
- ...
139
+ "arg1": { # With default
140
+ "flags": ["-a1", "--arg1"],
141
+ "default": "default_val"
142
+ },
143
+ "arg2": ("-a2", "--arg2"), # Without default (original format)
144
+ "arg3": ["-a3"], # Without default (list format)
145
+ "arg4": { # Flag with default True
146
+ "flags": ["-f"],
147
+ "default": True
148
+ }
83
149
  }
84
150
  ```
85
- And if the script is called via the command line:\n
86
- `python script.py -a1 "argument value" --arg2`\n
87
- ...it would return the following `Args` object:
88
- ```python
89
- Args(
90
- arg1_alias=ArgResult(exists=True, value="argument value"),
91
- arg2_alias=ArgResult(exists=True, value=None),
92
- ...
93
- )
94
- ```
95
- ...which can be accessed like this:\n
96
- - `Args.<arg_alias>.exists` is `True` if any of the specified
97
- args were found and `False` if not
98
- - `Args.<arg_alias>.value` the value from behind the found arg,
99
- `None` if no value was found"""
151
+ If the script is called via the command line:\n
152
+ `python script.py -a1 "value1" --arg2 -f`\n
153
+ ...it would return an `Args` object where:
154
+ - `args.arg1.exists` is `True`, `args.arg1.value` is `"value1"`
155
+ - `args.arg2.exists` is `True`, `args.arg2.value` is `True` (flag present without value)
156
+ - `args.arg3.exists` is `False`, `args.arg3.value` is `None` (not present, no default)
157
+ - `args.arg4.exists` is `True`, `args.arg4.value` is `True` (flag present, overrides default)
158
+ - If an arg defined in `find_args` is *not* present in the command line:
159
+ - `exists` will be `False`
160
+ - `value` will be the specified `default` value, or `None` if no default was specified.\n
161
+ ----------------------------------------------------------------
162
+ Normally if `allow_spaces` is false, it will take a space as
163
+ the end of an args value. If it is true, it will take spaces as
164
+ part of the value until the next arg is found.
165
+ (Multiple spaces will become one space in the value.)"""
100
166
  args = _sys.argv[1:]
167
+ args_len = len(args)
168
+ arg_lookup = {}
101
169
  results = {}
102
- for arg_key, arg_group in find_args.items():
103
- value = None
104
- exists = False
105
- for arg in arg_group:
106
- if arg in args:
107
- exists = True
108
- arg_index = args.index(arg)
109
- if arg_index + 1 < len(args) and not args[arg_index + 1].startswith("-"):
110
- value = String.to_type(args[arg_index + 1])
111
- break
112
- results[arg_key] = {"exists": exists, "value": value}
170
+ for alias, config in find_args.items():
171
+ flags = None
172
+ default_value = None
173
+ if isinstance(config, (list, tuple)):
174
+ flags = config
175
+ elif isinstance(config, dict):
176
+ if "flags" not in config:
177
+ raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
178
+ flags = config["flags"]
179
+ default_value = config.get("default")
180
+ if not isinstance(flags, (list, tuple)):
181
+ raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
182
+ else:
183
+ raise TypeError(f"Invalid configuration type for alias '{alias}'. Must be a list, tuple, or dict.")
184
+ results[alias] = {"exists": False, "value": default_value}
185
+ for flag in flags:
186
+ if flag in arg_lookup:
187
+ raise ValueError(
188
+ f"Duplicate flag '{flag}' found. It's assigned to both '{arg_lookup[flag]}' and '{alias}'."
189
+ )
190
+ arg_lookup[flag] = alias
191
+ i = 0
192
+ while i < args_len:
193
+ arg = args[i]
194
+ alias = arg_lookup.get(arg)
195
+ if alias:
196
+ results[alias]["exists"] = True
197
+ value_found_after_flag = False
198
+ if i + 1 < args_len and not args[i + 1].startswith("-"):
199
+ if not allow_spaces:
200
+ results[alias]["value"] = String.to_type(args[i + 1])
201
+ i += 1
202
+ value_found_after_flag = True
203
+ else:
204
+ value_parts = []
205
+ j = i + 1
206
+ while j < args_len and not args[j].startswith("-"):
207
+ value_parts.append(args[j])
208
+ j += 1
209
+ if value_parts:
210
+ results[alias]["value"] = String.to_type(" ".join(value_parts))
211
+ i = j - 1
212
+ value_found_after_flag = True
213
+ if not value_found_after_flag:
214
+ results[alias]["value"] = True
215
+ i += 1
113
216
  return Args(**results)
114
217
 
115
218
  @staticmethod
@@ -166,9 +269,10 @@ class Console:
166
269
  title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
167
270
  title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
168
271
  if format_linebreaks:
169
- prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(prompt).splitlines())
272
+ clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
273
+ prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
170
274
  prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
171
- prompt = f"\n{' ' * title_len}\t".join(prompt_lst)
275
+ prompt = f"\n{' ' * title_len}\t".join(Console.__add_back_removed_parts(list(prompt_lst), removals))
172
276
  else:
173
277
  prompt = str(prompt)
174
278
  if title == "":
@@ -185,6 +289,38 @@ class Console:
185
289
  end=end,
186
290
  )
187
291
 
292
+ @staticmethod
293
+ def __add_back_removed_parts(split_string: list[str], removals: tuple[tuple[int, str], ...]) -> list[str]:
294
+ """Adds back the removed parts into the split string parts at their original positions."""
295
+ lengths, cumulative_pos = [len(s) for s in split_string], [0]
296
+ for length in lengths:
297
+ cumulative_pos.append(cumulative_pos[-1] + length)
298
+ result, offset_adjusts = split_string.copy(), [0] * len(split_string)
299
+ last_idx, total_length = len(split_string) - 1, cumulative_pos[-1]
300
+
301
+ def find_string_part(pos: int) -> int:
302
+ left, right = 0, len(cumulative_pos) - 1
303
+ while left < right:
304
+ mid = (left + right) // 2
305
+ if cumulative_pos[mid] <= pos < cumulative_pos[mid + 1]:
306
+ return mid
307
+ elif pos < cumulative_pos[mid]:
308
+ right = mid
309
+ else:
310
+ left = mid + 1
311
+ return left
312
+
313
+ for pos, removal in removals:
314
+ if pos >= total_length:
315
+ result[last_idx] = result[last_idx] + removal
316
+ continue
317
+ i = find_string_part(pos)
318
+ adjusted_pos = (pos - cumulative_pos[i]) + offset_adjusts[i]
319
+ parts = [result[i][:adjusted_pos], removal, result[i][adjusted_pos:]]
320
+ result[i] = ''.join(parts)
321
+ offset_adjusts[i] += len(removal)
322
+ return result
323
+
188
324
  @staticmethod
189
325
  def debug(
190
326
  prompt: object = "Point in program reached.",
@@ -307,7 +443,7 @@ class Console:
307
443
  -----------------------------------------------------------------------------------
308
444
  The box content can be formatted with special formatting codes. For more detailed
309
445
  information about formatting codes, see `xx_format_codes` module documentation."""
310
- lines = [line.strip() for val in values for line in val.splitlines()]
446
+ lines = [line.rstrip() for val in values for line in val.splitlines()]
311
447
  unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
312
448
  max_line_len = max(len(line) for line in unfmt_lines)
313
449
  pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
@@ -409,7 +545,7 @@ class Console:
409
545
  last_console_width = 0
410
546
 
411
547
  def update_display(console_width: int) -> None:
412
- nonlocal select_all, last_line_count, last_console_width
548
+ nonlocal last_line_count, last_console_width
413
549
  lines = String.split_count(str(prompt) + (mask_char * len(result) if mask_char else result), console_width)
414
550
  line_count = len(lines)
415
551
  if (line_count > 1 or line_count < last_line_count) and not last_line_count == 1:
xulbux/xx_data.py CHANGED
@@ -2,13 +2,14 @@ from ._consts_ import COLOR
2
2
  from .xx_format_codes import FormatCodes
3
3
  from .xx_string import String
4
4
 
5
- from typing import TypeAlias, Optional, Union
5
+ from typing import TypeAlias, Optional, Union, Any
6
6
  import base64 as _base64
7
7
  import math as _math
8
8
  import re as _re
9
9
 
10
10
 
11
11
  DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
12
+ IndexIterable: TypeAlias = Union[list, tuple, set, frozenset]
12
13
 
13
14
 
14
15
  class Data:
@@ -42,16 +43,23 @@ class Data:
42
43
  @staticmethod
43
44
  def chars_count(data: DataStructure) -> int:
44
45
  """The sum of all the characters amount including the keys in dictionaries."""
46
+ chars_count = 0
45
47
  if isinstance(data, dict):
46
- return sum(len(str(k)) + len(str(v)) for k, v in data.items())
47
- return sum(len(str(item)) for item in data)
48
+ for k, v in data.items():
49
+ chars_count += len(str(k)) + (Data.chars_count(v) if isinstance(v, DataStructure) else len(str(v)))
50
+ elif isinstance(data, IndexIterable):
51
+ for item in data:
52
+ chars_count += Data.chars_count(item) if isinstance(item, DataStructure) else len(str(item))
53
+ return chars_count
48
54
 
49
55
  @staticmethod
50
56
  def strip(data: DataStructure) -> DataStructure:
51
57
  """Removes leading and trailing whitespaces from the data structure's items."""
52
58
  if isinstance(data, dict):
53
- return {k: Data.strip(v) for k, v in data.items()}
54
- return type(data)(map(Data.strip, data))
59
+ return {k.strip(): Data.strip(v) if isinstance(v, DataStructure) else v.strip() for k, v in data.items()}
60
+ if isinstance(data, IndexIterable):
61
+ return type(data)(Data.strip(item) if isinstance(item, DataStructure) else item.strip() for item in data)
62
+ return data
55
63
 
56
64
  @staticmethod
57
65
  def remove_empty_items(data: DataStructure, spaces_are_empty: bool = False) -> DataStructure:
@@ -59,18 +67,15 @@ class Data:
59
67
  If `spaces_are_empty` is true, it will count items with only spaces as empty."""
60
68
  if isinstance(data, dict):
61
69
  return {
62
- k: (
63
- v if not isinstance(v,
64
- (list, tuple, set, frozenset, dict)) else Data.remove_empty_items(v, spaces_are_empty)
65
- )
70
+ k: (v if not isinstance(v, DataStructure) else Data.remove_empty_items(v, spaces_are_empty))
66
71
  for k, v in data.items() if not String.is_empty(v, spaces_are_empty)
67
72
  }
68
- if isinstance(data, (list, tuple, set, frozenset)):
73
+ if isinstance(data, IndexIterable):
69
74
  return type(data)(
70
- item for item in ((
71
- item if not isinstance(item, (list, tuple, set, frozenset,
72
- dict)) else Data.remove_empty_items(item, spaces_are_empty)
73
- ) for item in data if not String.is_empty(item, spaces_are_empty)) if item not in ((), {}, set(), frozenset())
75
+ item for item in
76
+ ((item if not isinstance(item, DataStructure) else Data.remove_empty_items(item, spaces_are_empty))
77
+ for item in data if not String.is_empty(item, spaces_are_empty))
78
+ if item not in ([], (), {}, set(), frozenset())
74
79
  )
75
80
  return data
76
81
 
@@ -78,17 +83,25 @@ class Data:
78
83
  def remove_duplicates(data: DataStructure) -> DataStructure:
79
84
  """Removes all duplicates from the data structure."""
80
85
  if isinstance(data, dict):
81
- return {k: Data.remove_duplicates(v) for k, v in data.items()}
86
+ return {k: Data.remove_duplicates(v) if isinstance(v, DataStructure) else v for k, v in data.items()}
82
87
  if isinstance(data, (list, tuple)):
83
- return type(data)(
84
- Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
85
- for item in dict.fromkeys(data)
86
- )
88
+ result = []
89
+ for item in data:
90
+ processed_item = Data.remove_duplicates(item) if isinstance(item, DataStructure) else item
91
+ is_duplicate = False
92
+ for existing_item in result:
93
+ if processed_item == existing_item:
94
+ is_duplicate = True
95
+ break
96
+ if not is_duplicate:
97
+ result.append(processed_item)
98
+ return type(data)(result)
87
99
  if isinstance(data, (set, frozenset)):
88
- return type(data)(
89
- Data.remove_duplicates(item) if isinstance(item, (list, tuple, set, frozenset, dict)) else item
90
- for item in data
91
- )
100
+ processed_elements = set()
101
+ for item in data:
102
+ processed_item = Data.remove_duplicates(item) if isinstance(item, DataStructure) else item
103
+ processed_elements.add(processed_item)
104
+ return type(data)(processed_elements)
92
105
  return data
93
106
 
94
107
  @staticmethod
@@ -161,13 +174,13 @@ class Data:
161
174
  else:
162
175
  return None if s.lstrip().startswith(comment_start) else s.strip() or None
163
176
 
164
- def process_item(item: any) -> any:
177
+ def process_item(item: Any) -> Any:
165
178
  if isinstance(item, dict):
166
179
  return {
167
180
  k: v
168
181
  for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
169
182
  }
170
- if isinstance(item, (list, tuple, set, frozenset)):
183
+ if isinstance(item, IndexIterable):
171
184
  processed = (v for v in map(process_item, item) if v is not None)
172
185
  return type(item)(processed)
173
186
  if isinstance(item, str):
@@ -283,7 +296,7 @@ class Data:
283
296
  if ignore_not_found:
284
297
  return None
285
298
  raise KeyError(f"Key '{key}' not found in dict.")
286
- elif isinstance(data_obj, (list, tuple, set, frozenset)):
299
+ elif isinstance(data_obj, IndexIterable):
287
300
  try:
288
301
  idx = int(key)
289
302
  data_obj = list(data_obj)[idx] # CONVERT TO LIST FOR INDEXING
@@ -310,7 +323,7 @@ class Data:
310
323
  return results if len(results) > 1 else results[0] if results else None
311
324
 
312
325
  @staticmethod
313
- def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) -> any:
326
+ def get_value_by_path_id(data: DataStructure, path_id: str, get_key: bool = False) -> Any:
314
327
  """Retrieves the value from `data` using the provided `path_id`.\n
315
328
  -------------------------------------------------------------------------------------------------
316
329
  Input your `data` along with a `path_id` that was created before using `Data.get_path_id()`.
@@ -319,7 +332,7 @@ class Data:
319
332
  The function will return the value (or key) from the path ID location, as long as the structure
320
333
  of `data` hasn't changed since creating the path ID to that value."""
321
334
 
322
- def get_nested(data: DataStructure, path: list[int], get_key: bool) -> any:
335
+ def get_nested(data: DataStructure, path: list[int], get_key: bool) -> Any:
323
336
  parent = None
324
337
  for i, idx in enumerate(path):
325
338
  if isinstance(data, dict):
@@ -328,7 +341,7 @@ class Data:
328
341
  return keys[idx]
329
342
  parent = data
330
343
  data = data[keys[idx]]
331
- elif isinstance(data, (list, tuple, set, frozenset)):
344
+ elif isinstance(data, IndexIterable):
332
345
  if i == len(path) - 1 and get_key:
333
346
  if parent is None or not isinstance(parent, dict):
334
347
  raise ValueError("Cannot get key from a non-dict parent")
@@ -342,45 +355,39 @@ class Data:
342
355
  return get_nested(data, Data.__sep_path_id(path_id), get_key)
343
356
 
344
357
  @staticmethod
345
- def set_value_by_path_id(data: DataStructure, update_values: str | list[str], sep: str = "::") -> list | tuple | dict:
358
+ def set_value_by_path_id(data: DataStructure, update_values: dict[str, Any]) -> list | tuple | dict:
346
359
  """Updates the value/s from `update_values` in the `data`.\n
347
360
  --------------------------------------------------------------------------------
348
361
  Input a list, tuple or dict as `data`, along with `update_values`, which is a
349
- path ID that was created before using `Data.get_path_id()`, together with the
350
- new value to be inserted where the path ID points to. The path ID and the new
351
- value are separated by `sep`, which per default is `::`.\n
362
+ dictionary where keys are path IDs and values are the new values to insert:
363
+ { "1>": "new value", "path_id2": ["new value 1", "new value 2"], ... }
364
+ The path IDs should have been created using `Data.get_path_id()`.\n
352
365
  --------------------------------------------------------------------------------
353
366
  The value from path ID will be changed to the new value, as long as the
354
367
  structure of `data` hasn't changed since creating the path ID to that value."""
355
368
 
356
- def update_nested(data: DataStructure, path: list[int], value: any) -> DataStructure:
369
+ def update_nested(data: DataStructure, path: list[int], value: Any) -> DataStructure:
357
370
  if len(path) == 1:
358
371
  if isinstance(data, dict):
359
- keys = list(data.keys())
360
- data = dict(data)
372
+ keys, data = list(data.keys()), dict(data)
361
373
  data[keys[path[0]]] = value
362
- elif isinstance(data, (list, tuple, set, frozenset)):
363
- data = list(data)
374
+ elif isinstance(data, IndexIterable):
375
+ was_t, data = type(data), list(data)
364
376
  data[path[0]] = value
365
- data = type(data)(data)
377
+ data = was_t(data)
366
378
  else:
367
379
  if isinstance(data, dict):
368
- keys = list(data.keys())
369
- key = keys[path[0]]
370
- data = dict(data)
371
- data[key] = update_nested(data[key], path[1:], value)
372
- elif isinstance(data, (list, tuple, set, frozenset)):
373
- data = list(data)
380
+ keys, data = list(data.keys()), dict(data)
381
+ data[keys[path[0]]] = update_nested(data[keys[path[0]]], path[1:], value)
382
+ elif isinstance(data, IndexIterable):
383
+ was_t, data = type(data), list(data)
374
384
  data[path[0]] = update_nested(data[path[0]], path[1:], value)
375
- data = type(data)(data)
385
+ data = was_t(data)
376
386
  return data
377
387
 
378
- if isinstance(update_values, str):
379
- update_values = [update_values]
380
- valid_entries = [(parts[0].strip(), parts[1]) for update_value in update_values
381
- if len(parts := update_value.split(str(sep).strip())) == 2]
388
+ valid_entries = [(path_id, new_val) for path_id, new_val in update_values.items()]
382
389
  if not valid_entries:
383
- raise ValueError(f"No valid update_values found: {update_values}")
390
+ raise ValueError(f"No valid update_values found in dictionary: {update_values}")
384
391
  for path_id, new_val in valid_entries:
385
392
  path = Data.__sep_path_id(path_id)
386
393
  data = update_nested(data, path, new_val)
@@ -431,12 +438,12 @@ class Data:
431
438
  for k, v in punct_map.items()
432
439
  }
433
440
 
434
- def format_value(value: any, current_indent: int = None) -> str:
441
+ def format_value(value: Any, current_indent: int = None) -> str:
435
442
  if current_indent is not None and isinstance(value, dict):
436
443
  return format_dict(value, current_indent + indent)
437
444
  elif current_indent is not None and hasattr(value, "__dict__"):
438
445
  return format_dict(value.__dict__, current_indent + indent)
439
- elif current_indent is not None and isinstance(value, (list, tuple, set, frozenset)):
446
+ elif current_indent is not None and isinstance(value, IndexIterable):
440
447
  return format_sequence(value, current_indent + indent)
441
448
  elif isinstance(value, (bytes, bytearray)):
442
449
  obj_dict = Data.serialize_bytes(value)
xulbux/xx_env_path.py CHANGED
@@ -1,7 +1,3 @@
1
- """
2
- Functions for modifying and checking the systems environment-variables (especially the PATH object).
3
- """
4
-
5
1
  from .xx_path import Path
6
2
 
7
3
  import sys as _sys
@@ -29,33 +25,21 @@ class EnvPath:
29
25
  return _os.path.normpath(path) in [_os.path.normpath(p) for p in paths]
30
26
 
31
27
  @staticmethod
32
- def add_path(
33
- path: str = None,
34
- cwd: bool = False,
35
- base_dir: bool = False,
36
- ) -> None:
28
+ def add_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> None:
37
29
  """Add a path to the PATH environment variable."""
38
30
  path = EnvPath.__get(path, cwd, base_dir)
39
31
  if not EnvPath.has_path(path):
40
32
  EnvPath.__persistent(path, add=True)
41
33
 
42
34
  @staticmethod
43
- def remove_path(
44
- path: str = None,
45
- cwd: bool = False,
46
- base_dir: bool = False,
47
- ) -> None:
35
+ def remove_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> None:
48
36
  """Remove a path from the PATH environment variable."""
49
37
  path = EnvPath.__get(path, cwd, base_dir)
50
38
  if EnvPath.has_path(path):
51
39
  EnvPath.__persistent(path, remove=True)
52
40
 
53
41
  @staticmethod
54
- def __get(
55
- path: str = None,
56
- cwd: bool = False,
57
- base_dir: bool = False,
58
- ) -> list:
42
+ def __get(path: str = None, cwd: bool = False, base_dir: bool = False) -> list:
59
43
  """Get and/or normalize the paths.\n
60
44
  ------------------------------------------------------------------------------------
61
45
  Raise an error if no path is provided and neither `cwd` or `base_dir` is `True`."""
xulbux/xx_file.py CHANGED
@@ -1,11 +1,10 @@
1
1
  from .xx_string import String
2
- from .xx_path import Path
3
2
 
4
3
  import os as _os
5
4
 
6
5
 
7
6
  class SameContentFileExistsError(FileExistsError):
8
- pass
7
+ ...
9
8
 
10
9
 
11
10
  class File:
@@ -14,24 +13,34 @@ class File:
14
13
  def rename_extension(
15
14
  file: str,
16
15
  new_extension: str,
16
+ full_extension: bool = False,
17
17
  camel_case_filename: bool = False,
18
18
  ) -> str:
19
19
  """Rename the extension of a file.\n
20
20
  --------------------------------------------------------------------------
21
+ If `full_extension` is true, everything after the first dot in the
22
+ filename will be treated as the extension to replace. Otherwise, only the
23
+ part after the last dot is replaced.\n
21
24
  If the `camel_case_filename` parameter is true, the filename will be made
22
25
  CamelCase in addition to changing the files extension."""
23
- directory, filename_with_ext = _os.path.split(file)
24
- filename = filename_with_ext.split(".")[0]
26
+ normalized_file = _os.path.normpath(file)
27
+ directory, filename_with_ext = _os.path.split(normalized_file)
28
+ if full_extension:
29
+ try:
30
+ first_dot_index = filename_with_ext.index('.')
31
+ filename = filename_with_ext[:first_dot_index]
32
+ except ValueError:
33
+ filename = filename_with_ext
34
+ else:
35
+ filename, _ = _os.path.splitext(filename_with_ext)
25
36
  if camel_case_filename:
26
37
  filename = String.to_camel_case(filename)
38
+ if new_extension and not new_extension.startswith('.'):
39
+ new_extension = '.' + new_extension
27
40
  return _os.path.join(directory, f"{filename}{new_extension}")
28
41
 
29
42
  @staticmethod
30
- def create(
31
- file: str,
32
- content: str = "",
33
- force: bool = False,
34
- ) -> str:
43
+ def create(file: str, content: str = "", force: bool = False) -> str:
35
44
  """Create a file with ot without content.\n
36
45
  ----------------------------------------------------------------------
37
46
  The function will throw a `FileExistsError` if a file with the same
@@ -48,24 +57,3 @@ class File:
48
57
  f.write(content)
49
58
  full_path = _os.path.abspath(file)
50
59
  return full_path
51
-
52
- @staticmethod
53
- def extend_or_make_path(
54
- file: str,
55
- search_in: str | list[str] = None,
56
- prefer_base_dir: bool = True,
57
- correct_paths: bool = False,
58
- ) -> str:
59
- """Tries to find the file and extend the path to be absolute and if the file was not found:\n
60
- Generate the absolute path to the file in the CWD or the running program's base-directory.\n
61
- ----------------------------------------------------------------------------------------------
62
- If the `file` is not found in predefined directories, it will be searched in the `search_in`
63
- directory/directories. If the file is still not found, it will return the path to the file in
64
- the base-dir per default or to the file in the CWD if `prefer_base_dir` is set to `False`.\n
65
- ----------------------------------------------------------------------------------------------
66
- If `correct_paths` is true, it is possible to have typos in the `search_in` path/s and it
67
- will still find the file if it is under one of those paths."""
68
- try:
69
- return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
70
- except FileNotFoundError:
71
- return _os.path.join(Path.script_dir, file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)