xulbux 1.6.5__py3-none-any.whl → 1.6.7__py3-none-any.whl

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

Potentially problematic release.


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

xulbux/xx_data.py CHANGED
@@ -1,6 +1,9 @@
1
+ from ._consts_ import COLOR
2
+ from .xx_format_codes import FormatCodes
1
3
  from .xx_string import String
2
4
 
3
- from typing import TypeAlias, Union
5
+ from typing import TypeAlias, Optional, Union
6
+ import base64 as _base64
4
7
  import math as _math
5
8
  import re as _re
6
9
 
@@ -10,6 +13,32 @@ DataStructure: TypeAlias = Union[list, tuple, set, frozenset, dict]
10
13
 
11
14
  class Data:
12
15
 
16
+ @staticmethod
17
+ def serialize_bytes(data: bytes | bytearray) -> dict[str, str]:
18
+ """Converts bytes or bytearray to a JSON-compatible format (dictionary) with explicit keys."""
19
+ if isinstance(data, (bytes, bytearray)):
20
+ key = "bytearray" if isinstance(data, bytearray) else "bytes"
21
+ try:
22
+ return {key: data.decode("utf-8"), "encoding": "utf-8"}
23
+ except UnicodeDecodeError:
24
+ pass
25
+ return {key: _base64.b64encode(data).decode("utf-8"), "encoding": "base64"}
26
+ raise TypeError("Unsupported data type")
27
+
28
+ @staticmethod
29
+ def deserialize_bytes(obj: dict[str, str]) -> bytes | bytearray:
30
+ """Converts a JSON-compatible bytes/bytearray format (dictionary) back to its original type."""
31
+ for key in ("bytes", "bytearray"):
32
+ if key in obj and "encoding" in obj:
33
+ if obj["encoding"] == "utf-8":
34
+ data = obj[key].encode("utf-8")
35
+ elif obj["encoding"] == "base64":
36
+ data = _base64.b64decode(obj[key].encode("utf-8"))
37
+ else:
38
+ raise ValueError("Unknown encoding method")
39
+ return bytearray(data) if key == "bytearray" else data
40
+ raise ValueError("Invalid serialized data")
41
+
13
42
  @staticmethod
14
43
  def chars_count(data: DataStructure) -> int:
15
44
  """The sum of all the characters amount including the keys in dictionaries."""
@@ -31,26 +60,17 @@ class Data:
31
60
  if isinstance(data, dict):
32
61
  return {
33
62
  k: (
34
- v
35
- if not isinstance(v, (list, tuple, set, frozenset, dict))
36
- else Data.remove_empty_items(v, spaces_are_empty)
63
+ v if not isinstance(v,
64
+ (list, tuple, set, frozenset, dict)) else Data.remove_empty_items(v, spaces_are_empty)
37
65
  )
38
- for k, v in data.items()
39
- if not String.is_empty(v, spaces_are_empty)
66
+ for k, v in data.items() if not String.is_empty(v, spaces_are_empty)
40
67
  }
41
68
  if isinstance(data, (list, tuple, set, frozenset)):
42
69
  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())
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())
54
74
  )
55
75
  return data
56
76
 
@@ -131,7 +151,7 @@ class Data:
131
151
  rf"^((?:(?!{_re.escape(comment_start)}).)*){_re.escape(comment_start)}(?:(?:(?!{_re.escape(comment_end)}).)*)(?:{_re.escape(comment_end)})?(.*?)$"
132
152
  )
133
153
 
134
- def process_string(s: str) -> str | None:
154
+ def process_string(s: str) -> Optional[str]:
135
155
  if comment_end:
136
156
  match = pattern.match(s)
137
157
  if match:
@@ -144,7 +164,8 @@ class Data:
144
164
  def process_item(item: any) -> any:
145
165
  if isinstance(item, dict):
146
166
  return {
147
- k: v for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
167
+ k: v
168
+ for k, v in ((process_item(key), process_item(value)) for key, value in item.items()) if k is not None
148
169
  }
149
170
  if isinstance(item, (list, tuple, set, frozenset)):
150
171
  processed = (v for v in map(process_item, item) if v is not None)
@@ -174,9 +195,7 @@ class Data:
174
195
  The paths from `ignore_paths` and the `path_sep` parameter work exactly the same way as for
175
196
  the function `Data.get_path_id()`. See its documentation for more details."""
176
197
 
177
- def process_ignore_paths(
178
- ignore_paths: str | list[str],
179
- ) -> list[list[str]]:
198
+ def process_ignore_paths(ignore_paths: str | list[str], ) -> list[list[str]]:
180
199
  if isinstance(ignore_paths, str):
181
200
  ignore_paths = [ignore_paths]
182
201
  return [path.split(path_sep) for path in ignore_paths if path]
@@ -187,9 +206,9 @@ class Data:
187
206
  ignore_paths: list[list[str]],
188
207
  current_path: list[str] = [],
189
208
  ) -> bool:
190
- if any(current_path == path[: len(current_path)] for path in ignore_paths):
209
+ if any(current_path == path[:len(current_path)] for path in ignore_paths):
191
210
  return True
192
- if type(d1) != type(d2):
211
+ if type(d1) is not type(d2):
193
212
  return False
194
213
  if isinstance(d1, dict):
195
214
  if set(d1.keys()) != set(d2.keys()):
@@ -247,7 +266,7 @@ class Data:
247
266
  If `ignore_not_found` is `True`, the function will return `None` if the value is not found
248
267
  instead of raising an error."""
249
268
 
250
- def process_path(path: str, data_obj: list | tuple | set | frozenset | dict) -> str | None:
269
+ def process_path(path: str, data_obj: DataStructure) -> Optional[str]:
251
270
  keys = path.split(path_sep)
252
271
  path_ids = []
253
272
  max_id_length = 0
@@ -300,7 +319,7 @@ class Data:
300
319
  The function will return the value (or key) from the path ID location, as long as the structure
301
320
  of `data` hasn't changed since creating the path ID to that value."""
302
321
 
303
- def get_nested(data: list | tuple | set | frozenset | dict, path: list[int], get_key: bool) -> any:
322
+ def get_nested(data: DataStructure, path: list[int], get_key: bool) -> any:
304
323
  parent = None
305
324
  for i, idx in enumerate(path):
306
325
  if isinstance(data, dict):
@@ -323,11 +342,7 @@ class Data:
323
342
  return get_nested(data, Data.__sep_path_id(path_id), get_key)
324
343
 
325
344
  @staticmethod
326
- def set_value_by_path_id(
327
- data: DataStructure,
328
- update_values: str | list[str],
329
- sep: str = "::",
330
- ) -> list | tuple | dict:
345
+ def set_value_by_path_id(data: DataStructure, update_values: str | list[str], sep: str = "::") -> list | tuple | dict:
331
346
  """Updates the value/s from `update_values` in the `data`.\n
332
347
  --------------------------------------------------------------------------------
333
348
  Input a list, tuple or dict as `data`, along with `update_values`, which is a
@@ -338,9 +353,7 @@ class Data:
338
353
  The value from path ID will be changed to the new value, as long as the
339
354
  structure of `data` hasn't changed since creating the path ID to that value."""
340
355
 
341
- def update_nested(
342
- data: list | tuple | set | frozenset | dict, path: list[int], value: any
343
- ) -> list | tuple | set | frozenset | dict:
356
+ def update_nested(data: DataStructure, path: list[int], value: any) -> DataStructure:
344
357
  if len(path) == 1:
345
358
  if isinstance(data, dict):
346
359
  keys = list(data.keys())
@@ -364,11 +377,8 @@ class Data:
364
377
 
365
378
  if isinstance(update_values, str):
366
379
  update_values = [update_values]
367
- valid_entries = [
368
- (parts[0].strip(), parts[1])
369
- for update_value in update_values
370
- if len(parts := update_value.split(str(sep).strip())) == 2
371
- ]
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]
372
382
  if not valid_entries:
373
383
  raise ValueError(f"No valid update_values found: {update_values}")
374
384
  for path_id, new_val in valid_entries:
@@ -376,32 +386,6 @@ class Data:
376
386
  data = update_nested(data, path, new_val)
377
387
  return data
378
388
 
379
- @staticmethod
380
- def print(
381
- data: DataStructure,
382
- indent: int = 4,
383
- compactness: int = 1,
384
- max_width: int = 127,
385
- sep: str = ", ",
386
- end: str = "\n",
387
- as_json: bool = False,
388
- ) -> None:
389
- """Print nicely formatted data structures.\n
390
- ------------------------------------------------------------------------------
391
- The indentation spaces-amount can be set with with `indent`.
392
- There are three different levels of `compactness`:
393
- - `0` expands everything possible
394
- - `1` only expands if there's other lists, tuples or dicts inside of data or,
395
- if the data's content is longer than `max_width`
396
- - `2` keeps everything collapsed (all on one line)\n
397
- ------------------------------------------------------------------------------
398
- If `as_json` is set to `True`, the output will be in valid JSON format."""
399
- print(
400
- Data.to_str(data, indent, compactness, sep, max_width, as_json),
401
- end=end,
402
- flush=True,
403
- )
404
-
405
389
  @staticmethod
406
390
  def to_str(
407
391
  data: DataStructure,
@@ -410,6 +394,7 @@ class Data:
410
394
  max_width: int = 127,
411
395
  sep: str = ", ",
412
396
  as_json: bool = False,
397
+ _syntax_highlighting: dict[str, str] | bool = False,
413
398
  ) -> str:
414
399
  """Get nicely formatted data structure-strings.\n
415
400
  ------------------------------------------------------------------------------
@@ -421,106 +406,169 @@ class Data:
421
406
  - `2` keeps everything collapsed (all on one line)\n
422
407
  ------------------------------------------------------------------------------
423
408
  If `as_json` is set to `True`, the output will be in valid JSON format."""
409
+ if syntax_hl := _syntax_highlighting not in (None, False):
410
+ if _syntax_highlighting is True:
411
+ _syntax_highlighting = {}
412
+ elif not isinstance(_syntax_highlighting, dict):
413
+ raise TypeError(f"Expected 'syntax_highlighting' to be a dict or bool. Got: {type(_syntax_highlighting)}")
414
+ _syntax_hl = {
415
+ "str": (f"[{COLOR.blue}]", "[_c]"),
416
+ "number": (f"[{COLOR.magenta}]", "[_c]"),
417
+ "literal": (f"[{COLOR.cyan}]", "[_c]"),
418
+ "type": (f"[i|{COLOR.lightblue}]", "[_i|_c]"),
419
+ "punctuation": (f"[{COLOR.darkgray}]", "[_c]"),
420
+ }
421
+ _syntax_hl.update({
422
+ k: [f"[{v}]", "[_]"] if k in _syntax_hl and v not in ("", None) else ["", ""]
423
+ for k, v in _syntax_highlighting.items()
424
+ })
425
+ sep = f"{_syntax_hl['punctuation'][0]}{sep}{_syntax_hl['punctuation'][1]}"
426
+ punct_map = {"(": ("/(", "("), **{char: char for char in "'\":)[]{}"}}
427
+ punct = {
428
+ k: ((f"{_syntax_hl['punctuation'][0]}{v[0]}{_syntax_hl['punctuation'][1]}" if syntax_hl else v[1])
429
+ if isinstance(v, (list, tuple)) else
430
+ (f"{_syntax_hl['punctuation'][0]}{v}{_syntax_hl['punctuation'][1]}" if syntax_hl else v))
431
+ for k, v in punct_map.items()
432
+ }
424
433
 
425
- def format_value(value: any, current_indent: int) -> str:
426
- if isinstance(value, dict):
434
+ def format_value(value: any, current_indent: int = None) -> str:
435
+ if current_indent is not None and isinstance(value, dict):
427
436
  return format_dict(value, current_indent + indent)
428
- elif hasattr(value, "__dict__"):
437
+ elif current_indent is not None and hasattr(value, "__dict__"):
429
438
  return format_dict(value.__dict__, current_indent + indent)
430
- elif isinstance(value, (list, tuple, set, frozenset)):
439
+ elif current_indent is not None and isinstance(value, (list, tuple, set, frozenset)):
431
440
  return format_sequence(value, current_indent + indent)
441
+ elif isinstance(value, (bytes, bytearray)):
442
+ obj_dict = Data.serialize_bytes(value)
443
+ return (
444
+ format_dict(obj_dict, current_indent + indent) if as_json else (
445
+ f"{_syntax_hl['type'][0]}{(k := next(iter(obj_dict)))}{_syntax_hl['type'][1]}"
446
+ + format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent) if syntax_hl else
447
+ (k := next(iter(obj_dict)))
448
+ + format_sequence((obj_dict[k], obj_dict["encoding"]), current_indent + indent)
449
+ )
450
+ )
432
451
  elif isinstance(value, bool):
433
- return str(value).lower() if as_json else str(value)
452
+ val = str(value).lower() if as_json else str(value)
453
+ return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
434
454
  elif isinstance(value, (int, float)):
435
- return "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
455
+ val = "null" if as_json and (_math.isinf(value) or _math.isnan(value)) else str(value)
456
+ return f"{_syntax_hl['number'][0]}{val}{_syntax_hl['number'][1]}" if syntax_hl else val
436
457
  elif isinstance(value, complex):
437
- return f"[{value.real}, {value.imag}]" if as_json else str(value)
458
+ return (
459
+ format_value(str(value).strip("()")) if as_json else (
460
+ f"{_syntax_hl['type'][0]}complex{_syntax_hl['type'][1]}"
461
+ + format_sequence((value.real, value.imag), current_indent + indent)
462
+ if syntax_hl else f"complex{format_sequence((value.real, value.imag), current_indent + indent)}"
463
+ )
464
+ )
438
465
  elif value is None:
439
- return "null" if as_json else "None"
466
+ val = "null" if as_json else "None"
467
+ return f"{_syntax_hl['literal'][0]}{val}{_syntax_hl['literal'][1]}" if syntax_hl else val
440
468
  else:
441
- return '"' + String.escape(str(value), '"') + '"' if as_json else "'" + String.escape(str(value), "'") + "'"
469
+ return ((
470
+ punct['"'] + _syntax_hl["str"][0] + String.escape(str(value), '"') + _syntax_hl["str"][1]
471
+ + punct['"'] if syntax_hl else punct['"'] + String.escape(str(value), '"') + punct['"']
472
+ ) if as_json else (
473
+ punct["'"] + _syntax_hl["str"][0] + String.escape(str(value), "'") + _syntax_hl["str"][1]
474
+ + punct["'"] if syntax_hl else punct["'"] + String.escape(str(value), "'") + punct["'"]
475
+ ))
442
476
 
443
477
  def should_expand(seq: list | tuple | dict) -> bool:
444
478
  if compactness == 0:
445
479
  return True
446
480
  if compactness == 2:
447
481
  return False
448
- complex_items = sum(1 for item in seq if isinstance(item, (list, tuple, dict, set, frozenset)))
482
+ complex_types = (list, tuple, dict, set, frozenset) + ((bytes, bytearray) if as_json else ())
483
+ complex_items = sum(1 for item in seq if isinstance(item, complex_types))
449
484
  return (
450
- complex_items > 1
451
- or (complex_items == 1 and len(seq) > 1)
452
- or Data.chars_count(seq) + (len(seq) * len(sep)) > max_width
453
- )
454
-
455
- def format_key(k: any) -> str:
456
- return (
457
- '"' + String.escape(str(k), '"') + '"'
458
- if as_json
459
- else ("'" + String.escape(str(k), "'") + "'" if isinstance(k, str) else str(k))
485
+ complex_items > 1 or (complex_items == 1 and len(seq) > 1) or Data.chars_count(seq) +
486
+ (len(seq) * len(sep)) > max_width
460
487
  )
461
488
 
462
489
  def format_dict(d: dict, current_indent: int) -> str:
463
490
  if not d or compactness == 2:
464
- return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
491
+ return (
492
+ punct["{"]
493
+ + sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
494
+ for k, v in d.items()) + punct["}"]
495
+ )
465
496
  if not should_expand(d.values()):
466
- return "{" + sep.join(f"{format_key(k)}: {format_value(v, current_indent)}" for k, v in d.items()) + "}"
497
+ return (
498
+ punct["{"]
499
+ + sep.join(f"{format_value(k)}{punct[':']} {format_value(v, current_indent)}"
500
+ for k, v in d.items()) + punct["}"]
501
+ )
467
502
  items = []
468
- for key, value in d.items():
469
- formatted_value = format_value(value, current_indent)
470
- items.append(f'{" " * (current_indent + indent)}{format_key(key)}: {formatted_value}')
471
- return "{\n" + ",\n".join(items) + f'\n{" " * current_indent}}}'
503
+ for k, val in d.items():
504
+ formatted_value = format_value(val, current_indent)
505
+ items.append(f"{' ' * (current_indent + indent)}{format_value(k)}{punct[':']} {formatted_value}")
506
+ return punct["{"] + "\n" + f"{sep}\n".join(items) + f"\n{' ' * current_indent}" + punct["}"]
472
507
 
473
508
  def format_sequence(seq, current_indent: int) -> str:
474
509
  if as_json:
475
510
  seq = list(seq)
476
511
  if not seq or compactness == 2:
477
512
  return (
478
- "[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
479
- if isinstance(seq, list)
480
- else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
513
+ punct["["] + sep.join(format_value(item, current_indent)
514
+ for item in seq) + punct["]"] if isinstance(seq, list) else punct["("]
515
+ + sep.join(format_value(item, current_indent) for item in seq) + punct[")"]
481
516
  )
482
517
  if not should_expand(seq):
483
518
  return (
484
- "[" + sep.join(format_value(item, current_indent) for item in seq) + "]"
485
- if isinstance(seq, list)
486
- else "(" + sep.join(format_value(item, current_indent) for item in seq) + ")"
519
+ punct["["] + sep.join(format_value(item, current_indent)
520
+ for item in seq) + punct["]"] if isinstance(seq, list) else punct["("]
521
+ + sep.join(format_value(item, current_indent) for item in seq) + punct[")"]
487
522
  )
488
523
  items = [format_value(item, current_indent) for item in seq]
489
- formatted_items = ",\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
524
+ formatted_items = f"{sep}\n".join(f'{" " * (current_indent + indent)}{item}' for item in items)
490
525
  if isinstance(seq, list):
491
- return "[\n" + formatted_items + f'\n{" " * current_indent}]'
526
+ return f"{punct['[']}\n{formatted_items}\n{' ' * current_indent}{punct[']']}"
492
527
  else:
493
- return "(\n" + formatted_items + f'\n{" " * current_indent})'
528
+ return f"{punct['(']}\n{formatted_items}\n{' ' * current_indent}{punct[')']}"
494
529
 
495
- return format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0)
530
+ return _re.sub(r"\s+(?=\n)", "", format_dict(data, 0) if isinstance(data, dict) else format_sequence(data, 0))
496
531
 
497
532
  @staticmethod
498
- def _is_key(data: DataStructure, path_id: str) -> bool:
499
- """Returns `True` if the path ID points to a key in `data` and `False` otherwise.\n
500
- ------------------------------------------------------------------------------------
501
- Input a list, tuple or dict as `data`, along with `path_id`, which is a path ID
502
- that was created before using `Data.get_path_id()`."""
503
-
504
- def check_nested(data: list | tuple | set | frozenset | dict, path: list[int]) -> bool:
505
- for i, idx in enumerate(path):
506
- if isinstance(data, dict):
507
- keys = list(data.keys())
508
- if i == len(path) - 1:
509
- return True
510
- try:
511
- data = data[keys[idx]]
512
- except IndexError:
513
- return False
514
- elif isinstance(data, (list, tuple, set, frozenset)):
515
- return False
516
- else:
517
- raise TypeError(f"Unsupported type {type(data)} at path {path[:i+1]}")
518
- return False
519
-
520
- if not isinstance(data, dict):
521
- return False
522
- path = Data.__sep_path_id(path_id)
523
- return check_nested(data, path)
533
+ def print(
534
+ data: DataStructure,
535
+ indent: int = 4,
536
+ compactness: int = 1,
537
+ max_width: int = 127,
538
+ sep: str = ", ",
539
+ end: str = "\n",
540
+ as_json: bool = False,
541
+ syntax_highlighting: dict[str, str] | bool = {},
542
+ ) -> None:
543
+ """Print nicely formatted data structures.\n
544
+ ------------------------------------------------------------------------------
545
+ The indentation spaces-amount can be set with with `indent`.
546
+ There are three different levels of `compactness`:
547
+ - `0` expands everything possible
548
+ - `1` only expands if there's other lists, tuples or dicts inside of data or,
549
+ if the data's content is longer than `max_width`
550
+ - `2` keeps everything collapsed (all on one line)\n
551
+ ------------------------------------------------------------------------------
552
+ If `as_json` is set to `True`, the output will be in valid JSON format.\n
553
+ ------------------------------------------------------------------------------
554
+ The `syntax_highlighting` parameter is a dictionary with 5 keys for each part
555
+ of the data. The key's values are the formatting codes to apply to this data
556
+ part. The formatting can be changed by simply adding the key with the new
557
+ value inside the `syntax_highlighting` dictionary.\n
558
+ The keys with their default values are:
559
+ - `str: COLOR.blue`
560
+ - `number: COLOR.magenta`
561
+ - `literal: COLOR.cyan`
562
+ - `type: "i|" + COLOR.lightblue`
563
+ - `punctuation: COLOR.darkgray`\n
564
+ For no syntax highlighting, set `syntax_highlighting` to `False` or `None`.\n
565
+ ------------------------------------------------------------------------------
566
+ For more detailed information about formatting codes, see `xx_format_codes`
567
+ module documentation."""
568
+ FormatCodes.print(
569
+ Data.to_str(data, indent, compactness, max_width, sep, as_json, syntax_highlighting),
570
+ end=end,
571
+ )
524
572
 
525
573
  @staticmethod
526
574
  def __sep_path_id(path_id: str) -> list[int]:
@@ -528,4 +576,4 @@ class Data:
528
576
  raise ValueError(f"Invalid path ID: {path_id}")
529
577
  id_part_len = int(path_id.split(">")[0])
530
578
  path_ids_str = path_id.split(">")[1]
531
- return [int(path_ids_str[i : i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
579
+ return [int(path_ids_str[i:i + id_part_len]) for i in range(0, len(path_ids_str), id_part_len)]
xulbux/xx_env_path.py CHANGED
@@ -1,9 +1,5 @@
1
1
  """
2
- Functions for modifying and checking the systems environment-variables:
3
- - `EnvPath.paths()`
4
- - `EnvPath.has_path()`
5
- - `EnvPath.add_path()`
6
- - `EnvPath.remove_path()`
2
+ Functions for modifying and checking the systems environment-variables (especially the PATH object).
7
3
  """
8
4
 
9
5
  from .xx_path import Path
@@ -26,7 +22,7 @@ class EnvPath:
26
22
  if cwd:
27
23
  path = _os.getcwd()
28
24
  elif base_dir:
29
- path = Path.get(base_dir=True)
25
+ path = Path.script_dir
30
26
  elif path is None:
31
27
  raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
32
28
  paths = EnvPath.paths(as_list=True)
@@ -66,7 +62,7 @@ class EnvPath:
66
62
  if cwd:
67
63
  path = _os.getcwd()
68
64
  elif base_dir:
69
- path = Path.get(base_dir=True)
65
+ path = Path.script_dir
70
66
  elif path is None:
71
67
  raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
72
68
  return _os.path.normpath(path)
xulbux/xx_file.py CHANGED
@@ -4,6 +4,10 @@ from .xx_path import Path
4
4
  import os as _os
5
5
 
6
6
 
7
+ class SameContentFileExistsError(FileExistsError):
8
+ pass
9
+
10
+
7
11
  class File:
8
12
 
9
13
  @staticmethod
@@ -29,14 +33,16 @@ class File:
29
33
  force: bool = False,
30
34
  ) -> str:
31
35
  """Create a file with ot without content.\n
32
- ------------------------------------------------------------------------
33
- The function will throw a `FileExistsError` if the file already exists.
36
+ ----------------------------------------------------------------------
37
+ The function will throw a `FileExistsError` if a file with the same
38
+ name already exists and a `SameContentFileExistsError` if a file with
39
+ the same name and content already exists.
34
40
  To always overwrite the file, set the `force` parameter to `True`."""
35
41
  if _os.path.exists(file) and not force:
36
42
  with open(file, "r", encoding="utf-8") as existing_file:
37
43
  existing_content = existing_file.read()
38
44
  if existing_content == content:
39
- raise FileExistsError("Already created this file. (nothing changed)")
45
+ raise SameContentFileExistsError("Already created this file. (nothing changed)")
40
46
  raise FileExistsError("File already exists.")
41
47
  with open(file, "w", encoding="utf-8") as f:
42
48
  f.write(content)
@@ -62,4 +68,4 @@ class File:
62
68
  try:
63
69
  return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
64
70
  except FileNotFoundError:
65
- return _os.path.join(Path.get(base_dir=True), file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)
71
+ return _os.path.join(Path.script_dir, file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)
xulbux/xx_format_codes.py CHANGED
@@ -153,31 +153,33 @@ Per default, you can also use `+` and `-` to get lighter and darker `default_col
153
153
  from ._consts_ import ANSI
154
154
  from .xx_string import String
155
155
  from .xx_regex import Regex
156
- from .xx_color import *
156
+ from .xx_color import Color, rgba, hexa
157
157
 
158
+ from typing import Optional, Pattern
158
159
  import ctypes as _ctypes
159
160
  import regex as _rx
160
161
  import sys as _sys
161
162
  import re as _re
162
163
 
163
- _CONSOLE_ANSI_CONFIGURED = False
164
164
 
165
- _PREFIX = {
165
+ _CONSOLE_ANSI_CONFIGURED: bool = False
166
+
167
+ _PREFIX: dict[str, set[str]] = {
166
168
  "BG": {"background", "bg"},
167
169
  "BR": {"bright", "br"},
168
170
  }
169
- _PREFIX_RX = {
171
+ _PREFIX_RX: dict[str, str] = {
170
172
  "BG": rf"(?:{'|'.join(_PREFIX['BG'])})\s*:",
171
173
  "BR": rf"(?:{'|'.join(_PREFIX['BR'])})\s*:",
172
174
  }
173
- _COMPILED = { # PRECOMPILE REGULAR EXPRESSIONS
175
+ _COMPILED: dict[str, Pattern] = { # PRECOMPILE REGULAR EXPRESSIONS
174
176
  "*": _re.compile(r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]"),
175
177
  "*color": _re.compile(r"\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]"),
176
178
  "ansi_seq": _re.compile(ANSI.char + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"),
177
179
  "formatting": _rx.compile(
178
- Regex.brackets("[", "]", is_group=True)
180
+ Regex.brackets("[", "]", is_group=True, ignore_in_strings=False)
179
181
  + r"(?:\s*([/\\]?)\s*"
180
- + Regex.brackets("(", ")", is_group=True, ignore_in_strings=False)
182
+ + Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False)
181
183
  + r")?"
182
184
  ),
183
185
  "bg?_default": _re.compile(r"(?i)((?:" + _PREFIX_RX["BG"] + r")?)\s*default"),
@@ -294,33 +296,25 @@ class FormatCodes:
294
296
  reset_keys.append("_bg")
295
297
  break
296
298
  elif is_valid_color(k) or any(
297
- k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon) :])
298
- for prefix in _PREFIX["BR"]
299
- ):
299
+ k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon):])
300
+ for prefix in _PREFIX["BR"]):
300
301
  reset_keys.append("_color")
301
302
  else:
302
303
  reset_keys.append(f"_{k}")
303
304
  ansi_resets = [
304
- r
305
- for k in reset_keys
306
- if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(
307
- f"{ANSI.char}{ANSI.start}"
308
- )
305
+ r for k in reset_keys if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)
306
+ ).startswith(f"{ANSI.char}{ANSI.start}")
309
307
  ]
310
308
  else:
311
309
  ansi_resets = []
312
310
  if not (len(ansi_formats) == 1 and ansi_formats[0].count(f"{ANSI.char}{ANSI.start}") >= 1) and not all(
313
- f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats
314
- ):
311
+ f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats):
315
312
  return match.group(0)
316
313
  return (
317
- "".join(ansi_formats)
318
- + (
314
+ "".join(ansi_formats) + (
319
315
  f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
320
- if escaped and auto_reset_txt
321
- else auto_reset_txt if auto_reset_txt else ""
322
- )
323
- + ("" if escaped else "".join(ansi_resets))
316
+ if escaped and auto_reset_txt else auto_reset_txt if auto_reset_txt else ""
317
+ ) + ("" if escaped else "".join(ansi_resets))
324
318
  )
325
319
 
326
320
  string = "\n".join(_COMPILED["formatting"].sub(replace_keys, line) for line in string.split("\n"))
@@ -360,7 +354,7 @@ class FormatCodes:
360
354
  format_key: str = None,
361
355
  brightness_steps: int = None,
362
356
  _modifiers: tuple[str, str] = (ANSI.default_color_modifiers["lighten"], ANSI.default_color_modifiers["darken"]),
363
- ) -> str | None:
357
+ ) -> Optional[str]:
364
358
  """Get the `default_color` and lighter/darker versions of it as ANSI code."""
365
359
  if not brightness_steps or (format_key and _COMPILED["bg?_default"].search(format_key)):
366
360
  return (ANSI.seq_bg_color if format_key and _COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
@@ -399,14 +393,9 @@ class FormatCodes:
399
393
  for map_key in ANSI.codes_map:
400
394
  if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
401
395
  return ANSI.seq().format(
402
- next(
403
- (
404
- v
405
- for k, v in ANSI.codes_map.items()
406
- if format_key == k or (isinstance(k, tuple) and format_key in k)
407
- ),
408
- None,
409
- )
396
+ next((
397
+ v for k, v in ANSI.codes_map.items() if format_key == k or (isinstance(k, tuple) and format_key in k)
398
+ ), None)
410
399
  )
411
400
  rgb_match = _re.match(_COMPILED["rgb"], format_key)
412
401
  hex_match = _re.match(_COMPILED["hex"], format_key)
@@ -421,8 +410,7 @@ class FormatCodes:
421
410
  rgb = Color.to_rgba(hex_match.group(2))
422
411
  return (
423
412
  ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2])
424
- if is_bg
425
- else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
413
+ if is_bg else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
426
414
  )
427
415
  except Exception:
428
416
  pass
@@ -433,10 +421,11 @@ class FormatCodes:
433
421
  """Normalizes the given format key."""
434
422
  k_parts = format_key.replace(" ", "").lower().split(":")
435
423
  prefix_str = "".join(
436
- f"{prefix_key.lower()}:"
437
- for prefix_key, prefix_values in _PREFIX.items()
424
+ f"{prefix_key.lower()}:" for prefix_key, prefix_values in _PREFIX.items()
438
425
  if any(k_part in prefix_values for k_part in k_parts)
439
426
  )
440
427
  return prefix_str + ":".join(
441
- part for part in k_parts if part not in {val for values in _PREFIX.values() for val in values}
428
+ part for part in k_parts if part not in {val
429
+ for values in _PREFIX.values()
430
+ for val in values}
442
431
  )