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_env_path.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ Functions for modifying and checking the systems environment-variables:
3
+ - `EnvPath.paths()`
4
+ - `EnvPath.has_path()`
5
+ - `EnvPath.add_path()`
6
+ - `EnvPath.remove_path()`
7
+ """
8
+
9
+ from .xx_path import Path
10
+
11
+ import os as _os
12
+ import sys as _sys
13
+
14
+
15
+ class EnvPath:
16
+
17
+ @staticmethod
18
+ def paths(as_list: bool = False) -> str | list:
19
+ """Get the PATH environment variable."""
20
+ paths = _os.environ.get("PATH", "")
21
+ return paths.split(_os.pathsep) if as_list else paths
22
+
23
+ @staticmethod
24
+ def has_path(path: str = None, cwd: bool = False, base_dir: bool = False) -> bool:
25
+ """Check if a path is present in the PATH environment variable."""
26
+ if cwd:
27
+ path = _os.getcwd()
28
+ elif base_dir:
29
+ path = Path.get(base_dir=True)
30
+ elif path is None:
31
+ raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
32
+ paths = EnvPath.paths(as_list=True)
33
+ return _os.path.normpath(path) in [_os.path.normpath(p) for p in paths]
34
+
35
+ @staticmethod
36
+ def add_path(
37
+ path: str = None,
38
+ cwd: bool = False,
39
+ base_dir: bool = False,
40
+ ) -> None:
41
+ """Add a path to the PATH environment variable."""
42
+ path = EnvPath.__get(path, cwd, base_dir)
43
+ if not EnvPath.has_path(path):
44
+ EnvPath.__persistent(path, add=True)
45
+
46
+ @staticmethod
47
+ def remove_path(
48
+ path: str = None,
49
+ cwd: bool = False,
50
+ base_dir: bool = False,
51
+ ) -> None:
52
+ """Remove a path from the PATH environment variable."""
53
+ path = EnvPath.__get(path, cwd, base_dir)
54
+ if EnvPath.has_path(path):
55
+ EnvPath.__persistent(path, remove=True)
56
+
57
+ @staticmethod
58
+ def __get(
59
+ path: str = None,
60
+ cwd: bool = False,
61
+ base_dir: bool = False,
62
+ ) -> list:
63
+ """Get and/or normalize the paths.<br>
64
+ Raise an error if no path is provided and<br>
65
+ neither `cwd` or `base_dir` is `True`."""
66
+ if cwd:
67
+ path = _os.getcwd()
68
+ elif base_dir:
69
+ path = Path.get(base_dir=True)
70
+ elif path is None:
71
+ raise ValueError("A path must be provided or either 'cwd' or 'base_dir' must be True.")
72
+ return _os.path.normpath(path)
73
+
74
+ @staticmethod
75
+ def __persistent(path: str, add: bool = False, remove: bool = False) -> None:
76
+ """Add or remove a path from PATH persistently across sessions as well as the current session."""
77
+ if add == remove:
78
+ raise ValueError("Either add or remove must be True, but not both.")
79
+ current_paths = EnvPath.paths(as_list=True)
80
+ path = _os.path.normpath(path)
81
+ if remove:
82
+ current_paths = [p for p in current_paths if _os.path.normpath(p) != _os.path.normpath(path)]
83
+ elif add:
84
+ current_paths.append(path)
85
+ _os.environ["PATH"] = new_path = _os.pathsep.join(sorted(set(filter(bool, current_paths))))
86
+ if _sys.platform == "win32": # Windows
87
+ try:
88
+ import winreg as _winreg
89
+
90
+ key = _winreg.OpenKey(
91
+ _winreg.HKEY_CURRENT_USER,
92
+ "Environment",
93
+ 0,
94
+ _winreg.KEY_ALL_ACCESS,
95
+ )
96
+ _winreg.SetValueEx(key, "PATH", 0, _winreg.REG_EXPAND_SZ, new_path)
97
+ _winreg.CloseKey(key)
98
+ except ImportError:
99
+ print("Warning: Unable to make persistent changes on Windows.")
100
+ else: # UNIX-like (Linux/macOS)
101
+ shell_rc_file = _os.path.expanduser(
102
+ "~/.bashrc" if _os.path.exists(_os.path.expanduser("~/.bashrc")) else "~/.zshrc"
103
+ )
104
+ with open(shell_rc_file, "r+") as f:
105
+ content = f.read()
106
+ f.seek(0)
107
+ if remove:
108
+ new_content = [line for line in content.splitlines() if not line.endswith(f':{path}"')]
109
+ f.write("\n".join(new_content))
110
+ else:
111
+ f.write(f'{content.rstrip()}\n# Added by XulbuX\nexport PATH="{new_path}"\n')
112
+ f.truncate()
113
+ _os.system(f"source {shell_rc_file}")
xulbux/xx_file.py CHANGED
@@ -1,34 +1,11 @@
1
- from .xx_string import *
2
- from .xx_path import *
1
+ from .xx_string import String
2
+ from .xx_path import Path
3
3
 
4
4
  import os as _os
5
5
 
6
6
 
7
7
  class File:
8
8
 
9
- @staticmethod
10
- def _make_path(
11
- filename: str,
12
- filetype: str,
13
- search_in: str | list[str] = None,
14
- prefer_base_dir: bool = True,
15
- correct_path: bool = False,
16
- ) -> str:
17
- """Get the path to a file in the cwd, the base-dir, or predefined directories.\n
18
- --------------------------------------------------------------------------------------
19
- If the `filename` is not found in the above directories, it will be searched<br>
20
- in the `search_in` directory/directories. If the file is still not found, it will<br>
21
- return the path to the file in the base-dir per default or to the file in the<br>
22
- cwd if `prefer_base_dir` is set to `False`."""
23
- if not filename.lower().endswith(f".{filetype.lower()}"):
24
- filename = f"{filename}.{filetype.lower()}"
25
- try:
26
- return Path.extend(filename, search_in, True, correct_path)
27
- except FileNotFoundError:
28
- return (
29
- _os.path.join(Path.get(base_dir=True), filename) if prefer_base_dir else _os.path.join(_os.getcwd(), filename)
30
- )
31
-
32
9
  @staticmethod
33
10
  def rename_extension(file_path: str, new_extension: str) -> str:
34
11
  directory, filename_with_ext = _os.path.split(file_path)
@@ -54,3 +31,26 @@ class File:
54
31
  f.write(content)
55
32
  full_path = _os.path.abspath(file)
56
33
  return full_path
34
+
35
+ @staticmethod
36
+ def make_path(
37
+ filename: str,
38
+ filetype: str,
39
+ search_in: str | list[str] = None,
40
+ prefer_base_dir: bool = True,
41
+ correct_path: bool = False,
42
+ ) -> str:
43
+ """Create the path to a file in the cwd, the base-dir, or predefined directories.\n
44
+ --------------------------------------------------------------------------------------
45
+ If the `filename` is not found in the above directories, it will be searched<br>
46
+ in the `search_in` directory/directories. If the file is still not found, it will<br>
47
+ return the path to the file in the base-dir per default or to the file in the<br>
48
+ cwd if `prefer_base_dir` is set to `False`."""
49
+ if not filename.lower().endswith(f".{filetype.lower()}"):
50
+ filename = f"{filename}.{filetype.lower()}"
51
+ try:
52
+ return Path.extend(filename, search_in, True, correct_path)
53
+ except FileNotFoundError:
54
+ return (
55
+ _os.path.join(Path.get(base_dir=True), filename) if prefer_base_dir else _os.path.join(_os.getcwd(), filename)
56
+ )
xulbux/xx_format_codes.py CHANGED
@@ -68,10 +68,9 @@ Per default, you can also use `+` and `-` to get lighter and darker `default_col
68
68
  """
69
69
 
70
70
  from ._consts_ import ANSI
71
- from .xx_string import *
72
- from .xx_regex import *
71
+ from .xx_string import String
72
+ from .xx_regex import Regex
73
73
  from .xx_color import *
74
- from .xx_data import *
75
74
 
76
75
  from functools import lru_cache
77
76
  import ctypes as _ctypes
@@ -125,7 +124,8 @@ class FormatCodes:
125
124
  def to_ansi(
126
125
  string: str, default_color: hexa | rgba = None, brightness_steps: int = 20, _default_start: bool = True
127
126
  ) -> str:
128
- result = ""
127
+ result, bg_kwd, color_pref = string, {"bg"}, {"br", "bright"}
128
+
129
129
  if Color.is_valid_rgba(default_color, False):
130
130
  use_default = True
131
131
  elif Color.is_valid_hexa(default_color, False):
@@ -136,32 +136,48 @@ class FormatCodes:
136
136
  string = COMPILED["*"].sub(r"[\1_|default\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
137
137
  string = COMPILED["*color"].sub(r"[\1default\2]", string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
138
138
 
139
+ def is_valid_color(color: str) -> bool:
140
+ return color in ANSI.color_map or Color.is_valid_rgba(color) or Color.is_valid_hexa(color)
141
+
139
142
  def replace_keys(match: _re.Match) -> str:
140
- format_keys = match.group(1)
141
- esc = match.group(2)
143
+ formats = match.group(1)
144
+ escaped = match.group(2)
142
145
  auto_reset_txt = match.group(3)
143
- if not format_keys:
146
+ if auto_reset_txt and auto_reset_txt.count("[") > 0 and auto_reset_txt.count("]") > 0:
147
+ auto_reset_txt = FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)
148
+ if not formats:
144
149
  return match.group(0)
145
- format_keys = [k.strip() for k in format_keys.split("|") if k.strip()]
146
- ansi_formats = [FormatCodes.__get_replacement(k, default_color, brightness_steps) for k in format_keys]
147
- if auto_reset_txt and not esc:
148
- reset_keys = [
149
- (
150
- "_color"
151
- if k in ANSI.color_map or Color.is_valid_rgba(k) or Color.is_valid_hexa(k)
152
- else (
153
- "_bg"
154
- if ({"bg", "bright", "br"} & set(k.lower().split(":")))
155
- and len(k.split(":")) <= 3
156
- and any(
157
- k[i:] in ANSI.color_map or Color.is_valid_rgba(k[i:]) or Color.is_valid_hexa(k[i:])
158
- for i in range(len(k))
159
- )
160
- else f"_{k}"
161
- )
162
- )
163
- for k in format_keys
164
- ]
150
+ if formats.count("[") > 0 and formats.count("]") > 0:
151
+ formats = FormatCodes.to_ansi(formats, default_color, brightness_steps, False)
152
+ format_keys = [k.strip() for k in formats.split("|") if k.strip()]
153
+ ansi_formats = [
154
+ r if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)) != k else f"[{k}]"
155
+ for k in format_keys
156
+ ]
157
+ if auto_reset_txt and not escaped:
158
+ reset_keys = []
159
+ for k in format_keys:
160
+ k_lower = k.lower()
161
+ k_parts = k_lower.split(":")
162
+ k_set = set(k_parts)
163
+ if bg_kwd & k_set and len(k_parts) <= 3:
164
+ if k_set & color_pref:
165
+ for i in range(len(k)):
166
+ if is_valid_color(k[i:]):
167
+ reset_keys.extend(["_bg", "_color"])
168
+ break
169
+ else:
170
+ for i in range(len(k)):
171
+ if is_valid_color(k[i:]):
172
+ reset_keys.append("_bg")
173
+ break
174
+ elif is_valid_color(k) or any(
175
+ k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon) :])
176
+ for prefix in color_pref
177
+ ):
178
+ reset_keys.append("_color")
179
+ else:
180
+ reset_keys.append(f"_{k}")
165
181
  ansi_resets = [
166
182
  r
167
183
  for k in reset_keys
@@ -171,24 +187,27 @@ class FormatCodes:
171
187
  ]
172
188
  else:
173
189
  ansi_resets = []
174
- if not all(f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats):
190
+ if not (len(ansi_formats) == 1 and ansi_formats[0].count(f"{ANSI.char}{ANSI.start}") >= 1) and not all(
191
+ f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats
192
+ ):
175
193
  return match.group(0)
176
194
  return (
177
195
  "".join(ansi_formats)
178
196
  + (
179
197
  f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
180
- if esc and auto_reset_txt
198
+ if escaped and auto_reset_txt
181
199
  else auto_reset_txt if auto_reset_txt else ""
182
200
  )
183
- + ("" if esc else "".join(ansi_resets))
201
+ + ("" if escaped else "".join(ansi_resets))
184
202
  )
185
203
 
186
- return (
187
- (FormatCodes.__get_default_ansi(default_color) if _default_start else "")
188
- + (result := "\n".join(COMPILED["format"].sub(replace_keys, line) for line in string.split("\n")))
189
- if use_default
190
- else result
191
- )
204
+ result = "\n".join(COMPILED["format"].sub(replace_keys, line) for line in string.split("\n"))
205
+ return (FormatCodes.__get_default_ansi(default_color) if _default_start else "") + result if use_default else result
206
+
207
+ @staticmethod
208
+ def escape_ansi(ansi_string: str, escaped_char: str = ANSI.char_esc) -> str:
209
+ """Makes the string printable with the ANSI formats visible."""
210
+ return ansi_string.replace(ANSI.char, escaped_char)
192
211
 
193
212
  @staticmethod
194
213
  @lru_cache(maxsize=64)
xulbux/xx_json.py CHANGED
@@ -1,5 +1,5 @@
1
- from .xx_data import *
2
- from .xx_file import *
1
+ from .xx_data import Data
2
+ from .xx_file import File
3
3
 
4
4
  import json as _json
5
5
  import os as _os
@@ -22,7 +22,7 @@ class Json:
22
22
  the the section from `comment_start` to `comment_end` is ignored.<br>
23
23
  If `return_original` is set to `True`, the original JSON is returned<br>
24
24
  additionally. (returns: `[processed_json, original_json]`)"""
25
- file_path = File._make_path(json_file, "json", prefer_base_dir=True)
25
+ file_path = File.make_path(json_file, "json", prefer_base_dir=True)
26
26
  with open(file_path, "r") as f:
27
27
  content = f.read()
28
28
  try:
@@ -42,7 +42,7 @@ class Json:
42
42
  compactness: int = 1,
43
43
  force: bool = False,
44
44
  ) -> str:
45
- file_path = File._make_path(new_file, "json", prefer_base_dir=True)
45
+ file_path = File.make_path(new_file, "json", prefer_base_dir=True)
46
46
  if _os.path.exists(file_path) and not force:
47
47
  with open(file_path, "r", encoding="utf-8") as existing_f:
48
48
  existing_content = _json.load(existing_f)
xulbux/xx_regex.py CHANGED
@@ -22,16 +22,14 @@ class Regex:
22
22
  **`quote`** the quote type (single or double)<br>
23
23
  **`string`** everything inside the found quote pair\n
24
24
  ------------------------------------------------------------------------------------
25
- **Attention:** Requires non standard library `regex` not standard library `re`!
26
- """
25
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
27
26
  return r'(?P<quote>[\'"])(?P<string>(?:\\.|(?!\g<quote>).)*?)\g<quote>'
28
27
 
29
28
  @staticmethod
30
29
  def brackets(bracket1: str = "(", bracket2: str = ")", is_group: bool = False) -> str:
31
30
  """Matches everything inside brackets, including other nested brackets.\n
32
31
  ------------------------------------------------------------------------------------
33
- **Attention:** Requires non standard library `regex` not standard library `re`!
34
- """
32
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
35
33
  g, b1, b2 = (
36
34
  "" if is_group else "?:",
37
35
  _re.escape(bracket1) if len(bracket1) == 1 else bracket1,
@@ -51,10 +49,10 @@ class Regex:
51
49
  is_group: bool = False,
52
50
  ) -> str:
53
51
  """Matches everything except `disallowed_pattern`, unless the `disallowed_pattern` is found inside a string (`'...'` or `"..."`).\n
54
- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
55
- The `ignore_pattern` is just always ignored. For example if `disallowed_pattern` is `>` and `ignore_pattern` is `->`, the `->`-arrows will be allowed, even though they have `>` in them.<br>
56
- If `is_group` is `True`, you will be able to reference the matched content as a group (e.g. <code>match.group(<i>int</i>)</code> or <code>r'\\<i>int</i>'</code>).
57
- """
52
+ ------------------------------------------------------------------------------------------------------------------------------------
53
+ The `ignore_pattern` is just always ignored. For example if `disallowed_pattern` is `>` and `ignore_pattern` is `->`, the `->`<br>
54
+ -arrows will be allowed, even though they have `>` in them. If `is_group` is `True`, you will be able to reference the matched<br>
55
+ content as a group (e.g. <code>match.group(<i>int</i>)</code> or <code>r'\\<i>int</i>'</code>)."""
58
56
  return rf'({"" if is_group else "?:"}(?:(?!{ignore_pattern}).)*(?:(?!{Regex.outside_strings(disallowed_pattern)}).)*)'
59
57
 
60
58
  @staticmethod
@@ -64,12 +62,11 @@ class Regex:
64
62
  **`2`** the function's arguments\n
65
63
  If no `func_name` is given, it will match any function call.\n
66
64
  ------------------------------------------------------------------------------------
67
- **Attention:** Requires non standard library `regex` not standard library `re`!
68
- """
65
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
69
66
  return r"(?<=\b)(" + (func_name if func_name else r"[\w_]+") + r")\s*" + Regex.brackets("(", ")", is_group=True)
70
67
 
71
68
  @staticmethod
72
- def rgba_str(fix_sep: str = ",", allow_alpha: bool = False) -> str:
69
+ def rgba_str(fix_sep: str = ",", allow_alpha: bool = True) -> str:
73
70
  """Matches an RGBA color inside a string.\n
74
71
  --------------------------------------------------------------------------------
75
72
  The RGBA color can be in the formats (for `fix_sep = ','`):<br>
@@ -87,7 +84,7 @@ class Regex:
87
84
  `a` 0-1 (float: opacity)\n
88
85
  --------------------------------------------------------------------------------
89
86
  If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
90
- can be used to separate the RGB values, including just a space."""
87
+ can be used to separate the RGBA values, including just a space."""
91
88
  if fix_sep in (None, ""):
92
89
  fix_sep = r"[^0-9A-Z]"
93
90
  else:
@@ -105,7 +102,7 @@ class Regex:
105
102
  )
106
103
 
107
104
  @staticmethod
108
- def hsla_str(fix_sep: str = ",", allow_alpha: bool = False) -> str:
105
+ def hsla_str(fix_sep: str = ",", allow_alpha: bool = True) -> str:
109
106
  """Matches a HSLA color inside a string.\n
110
107
  --------------------------------------------------------------------------------
111
108
  The HSLA color can be in the formats (for `fix_sep = ','`):<br>
@@ -123,7 +120,7 @@ class Regex:
123
120
  `a` 0-1 (float: opacity)\n
124
121
  --------------------------------------------------------------------------------
125
122
  If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
126
- can be used to separate the HSL values, including just a space."""
123
+ can be used to separate the HSLA values, including just a space."""
127
124
  if fix_sep in (None, ""):
128
125
  fix_sep = r"[^0-9A-Z]"
129
126
  else:
@@ -139,3 +136,21 @@ class Regex:
139
136
  if allow_alpha
140
137
  else rf"(?ix)(?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}\s*\)?)"
141
138
  )
139
+
140
+ @staticmethod
141
+ def hexa_str(allow_alpha: bool = True) -> str:
142
+ """Matches a HEXA color inside a string.\n
143
+ --------------------------------------------------------------------------
144
+ The HEXA color can be in the formats (prefix `#`, `0x` or no prefix):<br>
145
+ `RGB`<br>
146
+ `RGBA` (if `allow_alpha=True`)<br>
147
+ `RRGGBB`<br>
148
+ `RRGGBBAA` (if `allow_alpha=True`)\n
149
+ --------------------------------------------------------------------------
150
+ ### Valid ranges:<br>
151
+ each channel from 0-9 and A-F (*case insensitive*)"""
152
+ return (
153
+ r"(?i)^(?:#|0x)?[0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3}$"
154
+ if allow_alpha
155
+ else r"(?i)^(?:#|0x)?[0-9A-F]{6}|[0-9A-F]{3}$"
156
+ )
xulbux/xx_string.py CHANGED
@@ -5,36 +5,54 @@ class String:
5
5
 
6
6
  @staticmethod
7
7
  def to_type(string: str) -> any:
8
- """Will convert a string to a type."""
9
- if string.lower() in ("true", "false"): # BOOLEAN
8
+ """Will convert a string to the found type."""
9
+ string = string.strip() # Clean up whitespace
10
+ # BOOLEAN
11
+ if _re.match(r"(?i)^(true|false)$", string):
10
12
  return string.lower() == "true"
11
- elif string.lower() in ("none", "null", "undefined"): # NONE
13
+ # NONE
14
+ elif _re.match(r"(?i)^(none|null|undefined)$", string):
12
15
  return None
13
- elif string.startswith("[") and string.endswith("]"): # LIST
14
- return [String.to_type(item.strip()) for item in string[1:-1].split(",") if item.strip()]
15
- elif string.startswith("(") and string.endswith(")"): # TUPLE
16
- return tuple(String.to_type(item.strip()) for item in string[1:-1].split(",") if item.strip())
17
- elif string.startswith("{") and string.endswith("}"): # SET
18
- return {String.to_type(item.strip()) for item in string[1:-1].split(",") if item.strip()}
19
- elif string.startswith("{") and string.endswith("}") and ":" in string: # DICTIONARY
16
+ # INTEGER
17
+ elif _re.match(r"^-?\d+$", string):
18
+ return int(string)
19
+ # FLOAT
20
+ elif _re.match(r"^-?\d+\.\d+$", string):
21
+ return float(string)
22
+ # COMPLEX
23
+ elif _re.match(r"^(-?\d+(\.\d+)?[+-]\d+(\.\d+)?j)$", string):
24
+ return complex(string)
25
+ # QUOTED STRING
26
+ elif _re.match(r'^["\'](.*)["\']$', string):
27
+ return string[1:-1]
28
+ # BYTES
29
+ elif _re.match(r"^b['\"](.*)['\"]$", string):
30
+ return bytes(string[2:-1], "utf-8")
31
+ # LIST
32
+ elif _re.match(r"^\[(.*)\]$", string):
33
+ return [
34
+ String.to_type(item.strip()) for item in _re.findall(r"(?:[^,\[\]]+|\[.*?\]|\(.*?\)|\{.*?\})+", string[1:-1])
35
+ ]
36
+ # TUPLE
37
+ elif _re.match(r"^\((.*)\)$", string):
38
+ return tuple(
39
+ String.to_type(item.strip()) for item in _re.findall(r"(?:[^,\(\)]+|\[.*?\]|\(.*?\)|\{.*?\})+", string[1:-1])
40
+ )
41
+ # DICTIONARY
42
+ elif _re.match(r"^\{(.*)\}$", string) and ":" in string:
20
43
  return {
21
44
  String.to_type(k.strip()): String.to_type(v.strip())
22
- for k, v in (item.split(":") for item in string[1:-1].split(",") if item.strip())
45
+ for k, v in _re.findall(
46
+ r"((?:[^:,{}]+|\[.*?\]|\(.*?\)|\{.*?\})+)\s*:\s*((?:[^:,{}]+|\[.*?\]|\(.*?\)|\{.*?\})+)", string[1:-1]
47
+ )
23
48
  }
24
- try: # NUMBER (INT OR FLOAT)
25
- if "." in string or "e" in string.lower():
26
- return float(string)
27
- else:
28
- return int(string)
29
- except ValueError:
30
- pass
31
- if string.startswith(("'", '"')) and string.endswith(("'", '"')): # STRING (WITH OR WITHOUT QUOTES)
32
- return string[1:-1]
33
- try: # COMPLEX
34
- return complex(string)
35
- except ValueError:
36
- pass
37
- return string # IF NOTHING ELSE MATCHES, RETURN AS IS
49
+ # SET
50
+ elif _re.match(r"^\{(.*?)\}$", string):
51
+ return {
52
+ String.to_type(item.strip()) for item in _re.findall(r"(?:[^,{}]+|\[.*?\]|\(.*?\)|\{.*?\})+", string[1:-1])
53
+ }
54
+ # RETURN AS IS (str)
55
+ return string
38
56
 
39
57
  @staticmethod
40
58
  def normalize_spaces(string: str, tab_spaces: int = 4) -> str:
@@ -55,11 +73,39 @@ class String:
55
73
  .replace("\u200A", " ")
56
74
  )
57
75
 
76
+ @staticmethod
77
+ def escape(string: str, str_quotes: str = '"') -> str:
78
+ """Escapes the special characters and quotes inside a string.\n
79
+ ----------------------------------------------------------------------------
80
+ `str_quotes` can be either `"` or `'` and should match the quotes,<br>
81
+ the string will be put inside of. So if your string will be `"string"`,<br>
82
+ you should pass `"` to the parameter `str_quotes`.<br>
83
+ That way, if the string includes the same quotes, they will be escaped."""
84
+ string = (
85
+ string.replace("\\", r"\\")
86
+ .replace("\n", r"\n")
87
+ .replace("\r", r"\r")
88
+ .replace("\t", r"\t")
89
+ .replace("\b", r"\b")
90
+ .replace("\f", r"\f")
91
+ .replace("\a", r"\a")
92
+ )
93
+ if str_quotes == '"':
94
+ string = string.replace(r"\\'", "'").replace(r'"', r"\"")
95
+ elif str_quotes == "'":
96
+ string = string.replace(r'\\"', '"').replace(r"'", r"\'")
97
+ return string
98
+
99
+ @staticmethod
100
+ def is_empty(string: str, spaces_are_empty: bool = False):
101
+ """Returns `True` if the string is empty and `False` otherwise.<br>
102
+ If `spaces_are_empty` is true, it will also return `True` if the string is only spaces."""
103
+ return (string in (None, "")) or (spaces_are_empty and isinstance(string, str) and not string.strip())
104
+
58
105
  @staticmethod
59
106
  def single_char_repeats(string: str, char: str) -> int | bool:
60
107
  """If the string consists of only the same `char`, it returns the number of times it is present.<br>
61
- If the string doesn't consist of only the same character, it returns `False`.
62
- """
108
+ If the string doesn't consist of only the same character, it returns `False`."""
63
109
  if len(string) == len(char) * string.count(char):
64
110
  return string.count(char)
65
111
  else:
@@ -68,20 +114,27 @@ class String:
68
114
  @staticmethod
69
115
  def decompose(case_string: str, seps: str = "-_", lower_all: bool = True) -> list[str]:
70
116
  """Will decompose the string (*any type of casing, also mixed*) into parts."""
71
- return [(part.lower() if lower_all else part) for part in _re.split(rf"(?<=[a-z])(?=[A-Z])|[{seps}]", case_string)]
117
+ return [
118
+ (part.lower() if lower_all else part)
119
+ for part in _re.split(rf"(?<=[a-z])(?=[A-Z])|[{_re.escape(seps)}]", case_string)
120
+ ]
72
121
 
73
122
  @staticmethod
74
- def to_camel_case(string: str) -> str:
75
- """Will convert the string of any type of casing to camel case."""
76
- return "".join(part.capitalize() for part in String.decompose(string))
123
+ def to_camel_case(string: str, upper: bool = True) -> str:
124
+ """Will convert the string of any type of casing to UpperCamelCase or lowerCamelCase if `upper` is false."""
125
+ return (
126
+ (parts := String.decompose(string))[0].lower()
127
+ if upper
128
+ else "" + "".join(part.capitalize() for part in (parts[1:] if upper else parts))
129
+ )
77
130
 
78
131
  @staticmethod
79
- def to_snake_case(string: str, sep: str = "_", screaming: bool = False) -> str:
80
- """Will convert the string of any type of casing to snake case."""
81
- return sep.join(part.upper() if screaming else part for part in String.decompose(string))
132
+ def to_delimited_case(string: str, delimiter: str = "_", screaming: bool = False) -> str:
133
+ """Will convert the string of any type of casing to casing delimited by `delimiter`."""
134
+ return delimiter.join(part.upper() if screaming else part for part in String.decompose(string))
82
135
 
83
136
  @staticmethod
84
- def get_string_lines(string: str, remove_empty_lines: bool = False) -> list[str]:
137
+ def get_lines(string: str, remove_empty_lines: bool = False) -> list[str]:
85
138
  """Will split the string into lines."""
86
139
  if not remove_empty_lines:
87
140
  return string.splitlines()
@@ -99,46 +152,10 @@ class String:
99
152
  ----------------------------------------------------------------------------------------------
100
153
  If `max_consecutive` is `0`, it will remove all consecutive empty lines.<br>
101
154
  If `max_consecutive` is bigger than `0`, it will only allow `max_consecutive` consecutive<br>
102
- empty lines and everything above it will be cut down to `max_consecutive` empty lines.
103
- """
155
+ empty lines and everything above it will be cut down to `max_consecutive` empty lines."""
104
156
  return _re.sub(r"(\n\s*){2,}", r"\1" * (max_consecutive + 1), string)
105
157
 
106
158
  @staticmethod
107
159
  def split_count(string: str, count: int) -> list[str]:
108
160
  """Will split the string every `count` characters."""
109
161
  return [string[i : i + count] for i in range(0, len(string), count)]
110
-
111
- @staticmethod
112
- def multi_strip(string: str, strip_chars: str = " _-") -> str:
113
- """Will remove all leading and trailing `strip_chars` from the string."""
114
- for char in string:
115
- if char in strip_chars:
116
- string = string[1:]
117
- else:
118
- break
119
- for char in string[::-1]:
120
- if char in strip_chars:
121
- string = string[:-1]
122
- else:
123
- break
124
- return string
125
-
126
- @staticmethod
127
- def multi_lstrip(string: str, strip_chars: str = " _-") -> str:
128
- """Will remove all leading `strip_chars` from the string."""
129
- for char in string:
130
- if char in strip_chars:
131
- string = string[1:]
132
- else:
133
- break
134
- return string
135
-
136
- @staticmethod
137
- def multi_rstrip(string: str, strip_chars: str = " _-") -> str:
138
- """Will remove all trailing `strip_chars` from the string."""
139
- for char in string[::-1]:
140
- if char in strip_chars:
141
- string = string[:-1]
142
- else:
143
- break
144
- return string