xulbux 1.9.5__cp311-cp311-macosx_11_0_arm64.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.
Files changed (43) hide show
  1. 455848faf89d8974b22a__mypyc.cpython-311-darwin.so +0 -0
  2. xulbux/__init__.cpython-311-darwin.so +0 -0
  3. xulbux/__init__.py +46 -0
  4. xulbux/base/consts.cpython-311-darwin.so +0 -0
  5. xulbux/base/consts.py +172 -0
  6. xulbux/base/decorators.cpython-311-darwin.so +0 -0
  7. xulbux/base/decorators.py +28 -0
  8. xulbux/base/exceptions.cpython-311-darwin.so +0 -0
  9. xulbux/base/exceptions.py +23 -0
  10. xulbux/base/types.cpython-311-darwin.so +0 -0
  11. xulbux/base/types.py +118 -0
  12. xulbux/cli/help.cpython-311-darwin.so +0 -0
  13. xulbux/cli/help.py +77 -0
  14. xulbux/code.cpython-311-darwin.so +0 -0
  15. xulbux/code.py +137 -0
  16. xulbux/color.cpython-311-darwin.so +0 -0
  17. xulbux/color.py +1331 -0
  18. xulbux/console.cpython-311-darwin.so +0 -0
  19. xulbux/console.py +2069 -0
  20. xulbux/data.cpython-311-darwin.so +0 -0
  21. xulbux/data.py +798 -0
  22. xulbux/env_path.cpython-311-darwin.so +0 -0
  23. xulbux/env_path.py +123 -0
  24. xulbux/file.cpython-311-darwin.so +0 -0
  25. xulbux/file.py +74 -0
  26. xulbux/file_sys.cpython-311-darwin.so +0 -0
  27. xulbux/file_sys.py +266 -0
  28. xulbux/format_codes.cpython-311-darwin.so +0 -0
  29. xulbux/format_codes.py +722 -0
  30. xulbux/json.cpython-311-darwin.so +0 -0
  31. xulbux/json.py +200 -0
  32. xulbux/regex.cpython-311-darwin.so +0 -0
  33. xulbux/regex.py +247 -0
  34. xulbux/string.cpython-311-darwin.so +0 -0
  35. xulbux/string.py +161 -0
  36. xulbux/system.cpython-311-darwin.so +0 -0
  37. xulbux/system.py +313 -0
  38. xulbux-1.9.5.dist-info/METADATA +271 -0
  39. xulbux-1.9.5.dist-info/RECORD +43 -0
  40. xulbux-1.9.5.dist-info/WHEEL +6 -0
  41. xulbux-1.9.5.dist-info/entry_points.txt +2 -0
  42. xulbux-1.9.5.dist-info/licenses/LICENSE +21 -0
  43. xulbux-1.9.5.dist-info/top_level.txt +2 -0
Binary file
xulbux/json.py ADDED
@@ -0,0 +1,200 @@
1
+ """
2
+ This module provides the `Json` class, which includes methods to read,
3
+ create and update JSON files, with support for comments inside the JSON data.
4
+ """
5
+
6
+ from .file_sys import FileSys
7
+ from .data import Data
8
+ from .file import File
9
+
10
+ from typing import Literal, Any, cast
11
+ from pathlib import Path
12
+ import json as _json
13
+
14
+
15
+ class Json:
16
+ """This class provides methods to read, create and update JSON files,
17
+ with support for comments inside the JSON data."""
18
+
19
+ @classmethod
20
+ def read(
21
+ cls,
22
+ json_file: Path | str,
23
+ comment_start: str = ">>",
24
+ comment_end: str = "<<",
25
+ return_original: bool = False,
26
+ ) -> dict | tuple[dict, dict]:
27
+ """Read JSON files, ignoring comments.\n
28
+ ------------------------------------------------------------------------------------
29
+ - `json_file` -⠀the path (relative or absolute) to the JSON file to read
30
+ - `comment_start` -⠀the string that indicates the start of a comment
31
+ - `comment_end` -⠀the string that indicates the end of a comment
32
+ - `return_original` -⠀if true, the original JSON data is returned additionally:<br>
33
+ ```python
34
+ (processed_json, original_json)
35
+ ```\n
36
+ ------------------------------------------------------------------------------------
37
+ For more detailed information about the comment handling,
38
+ see the `Data.remove_comments()` method documentation."""
39
+ if (json_path := Path(json_file) if isinstance(json_file, str) else json_file).suffix != ".json":
40
+ json_path = json_path.with_suffix(".json")
41
+ file_path = FileSys.extend_or_make_path(json_path, prefer_script_dir=True)
42
+
43
+ with open(file_path, "r") as file:
44
+ content = file.read()
45
+
46
+ try:
47
+ data = _json.loads(content)
48
+ except _json.JSONDecodeError as e:
49
+ fmt_error = "\n ".join(str(e).splitlines())
50
+ raise ValueError(f"Error parsing JSON in {file_path!r}:\n {fmt_error}") from e
51
+
52
+ if not (processed_data := dict(Data.remove_comments(data, comment_start, comment_end))):
53
+ raise ValueError(f"The JSON file {file_path!r} is empty or contains only comments.")
54
+
55
+ return (processed_data, data) if return_original else processed_data
56
+
57
+ @classmethod
58
+ def create(
59
+ cls,
60
+ json_file: Path | str,
61
+ data: dict,
62
+ indent: int = 2,
63
+ compactness: Literal[0, 1, 2] = 1,
64
+ force: bool = False,
65
+ ) -> Path:
66
+ """Create a nicely formatted JSON file from a dictionary.\n
67
+ ---------------------------------------------------------------------------
68
+ - `json_file` -⠀the path (relative or absolute) to the JSON file to create
69
+ - `data` -⠀the dictionary data to write to the JSON file
70
+ - `indent` -⠀the amount of spaces to use for indentation
71
+ - `compactness` -⠀can be `0`, `1` or `2` and indicates how compact
72
+ the data should be formatted (see `Data.render()` for more info)
73
+ - `force` -⠀if true, will overwrite existing files
74
+ without throwing an error (errors explained below)\n
75
+ ---------------------------------------------------------------------------
76
+ The method will throw a `FileExistsError` if a file with the same
77
+ name already exists and a `SameContentFileExistsError` if a file
78
+ with the same name and same content already exists."""
79
+ if (json_path := Path(json_file) if isinstance(json_file, str) else json_file).suffix != ".json":
80
+ json_path = json_path.with_suffix(".json")
81
+
82
+ file_path = FileSys.extend_or_make_path(json_path, prefer_script_dir=True)
83
+ File.create(
84
+ file_path=file_path,
85
+ content=Data.render(
86
+ data=data,
87
+ indent=indent,
88
+ compactness=compactness,
89
+ as_json=True,
90
+ syntax_highlighting=False,
91
+ ),
92
+ force=force,
93
+ )
94
+
95
+ return file_path
96
+
97
+ @classmethod
98
+ def update(
99
+ cls,
100
+ json_file: Path | str,
101
+ update_values: dict[str, Any],
102
+ comment_start: str = ">>",
103
+ comment_end: str = "<<",
104
+ path_sep: str = "->",
105
+ ) -> None:
106
+ """Update single/multiple values inside JSON files,
107
+ without needing to know the rest of the data.\n
108
+ -----------------------------------------------------------------------------------
109
+ - `json_file` -⠀the path (relative or absolute) to the JSON file to update
110
+ - `update_values` -⠀a dictionary with the paths to the values to update
111
+ and the new values to set (see explanation below – section 2)
112
+ - `comment_start` -⠀the string that indicates the start of a comment
113
+ - `comment_end` -⠀the string that indicates the end of a comment
114
+ - `path_sep` -⠀the separator used inside the value-paths in `update_values`\n
115
+ -----------------------------------------------------------------------------------
116
+ For more detailed information about the comment handling,
117
+ see the `Data.remove_comments()` method documentation.\n
118
+ -----------------------------------------------------------------------------------
119
+ The `update_values` is a dictionary, where the keys are the paths
120
+ to the data to update, and the values are the new values to set.\n
121
+ For example for this JSON data:
122
+ ```python
123
+ {
124
+ "healthy": {
125
+ "fruits": ["apples", "bananas", "oranges"],
126
+ "vegetables": ["carrots", "broccoli", "celery"]
127
+ }
128
+ }
129
+ ```
130
+ … the `update_values` dictionary could look like this:
131
+ ```python
132
+ {
133
+ # CHANGE FIRST LIST-VALUE UNDER 'fruits' TO "strawberries"
134
+ "healthy->fruits->0": "strawberries",
135
+ # CHANGE VALUE OF KEY 'vegetables' TO [1, 2, 3]
136
+ "healthy->vegetables": [1, 2, 3]
137
+ }
138
+ ```
139
+ In this example, if you want to change the value of `"apples"`,
140
+ you can use `healthy->fruits->apples` as the value-path.<br>
141
+ If you don't know that the first list item is `"apples"`,
142
+ you can use the items list index inside the value-path, so `healthy->fruits->0`.\n
143
+ ⇾ If the given value-path doesn't exist, it will be created."""
144
+ processed_data, data = cls.read(
145
+ json_file=json_file,
146
+ comment_start=comment_start,
147
+ comment_end=comment_end,
148
+ return_original=True,
149
+ )
150
+
151
+ update: dict[str, Any] = {}
152
+ for val_path, new_val in update_values.items():
153
+ try:
154
+ if (path_id := Data.get_path_id(data=processed_data, value_paths=val_path, path_sep=path_sep)) is not None:
155
+ update[cast(str, path_id)] = new_val
156
+ else:
157
+ data = cls._create_nested_path(data, val_path.split(path_sep), new_val)
158
+ except Exception:
159
+ data = cls._create_nested_path(data, val_path.split(path_sep), new_val)
160
+
161
+ if update:
162
+ data = Data.set_value_by_path_id(data, update)
163
+
164
+ cls.create(json_file=json_file, data=dict(data), force=True)
165
+
166
+ @staticmethod
167
+ def _create_nested_path(data_obj: dict, path_keys: list[str], value: Any) -> dict:
168
+ """Internal method that creates nested dictionaries/lists based on the
169
+ given path keys and sets the specified value at the end of the path."""
170
+ last_idx, current = len(path_keys) - 1, data_obj
171
+
172
+ for i, key in enumerate(path_keys):
173
+ if i == last_idx:
174
+ if isinstance(current, dict):
175
+ current[key] = value
176
+ elif isinstance(current, list) and key.isdigit():
177
+ idx = int(key)
178
+ while len(current) <= idx:
179
+ current.append(None)
180
+ current[idx] = value
181
+ else:
182
+ raise TypeError(f"Cannot set key '{key}' on {type(current)}")
183
+
184
+ else:
185
+ next_key = path_keys[i + 1]
186
+ if isinstance(current, dict):
187
+ if key not in current:
188
+ current[key] = [] if next_key.isdigit() else {}
189
+ current = current[key]
190
+ elif isinstance(current, list) and key.isdigit():
191
+ idx = int(key)
192
+ while len(current) <= idx:
193
+ current.append(None)
194
+ if current[idx] is None:
195
+ current[idx] = [] if next_key.isdigit() else {}
196
+ current = current[idx]
197
+ else:
198
+ raise TypeError(f"Cannot navigate through {type(current)}")
199
+
200
+ return data_obj
Binary file
xulbux/regex.py ADDED
@@ -0,0 +1,247 @@
1
+ """
2
+ This module provides the `Regex` class, which includes methods
3
+ to dynamically generate complex regex patterns for common use cases.
4
+ """
5
+
6
+ from .base.decorators import mypyc_attr
7
+
8
+ from typing import Optional
9
+ import regex as _rx
10
+ import re as _re
11
+
12
+
13
+ class Regex:
14
+ """This class provides methods to dynamically generate complex regex patterns for common use cases."""
15
+
16
+ @classmethod
17
+ def quotes(cls) -> str:
18
+ """Matches pairs of quotes. (strings)\n
19
+ --------------------------------------------------------------------------------
20
+ Will create two named groups:
21
+ - `quote` the quote type (single or double)
22
+ - `string` everything inside the found quote pair\n
23
+ ---------------------------------------------------------------------------------
24
+ Attention: Requires non-standard library `regex`, not standard library `re`!"""
25
+ return r"""(?P<quote>["'])(?P<string>(?:\\.|(?!\g<quote>).)*?)\g<quote>"""
26
+
27
+ @classmethod
28
+ def brackets(
29
+ cls,
30
+ bracket1: str = "(",
31
+ bracket2: str = ")",
32
+ is_group: bool = False,
33
+ strip_spaces: bool = False,
34
+ ignore_in_strings: bool = True,
35
+ ) -> str:
36
+ """Matches everything inside pairs of brackets, including other nested brackets.\n
37
+ ---------------------------------------------------------------------------------------
38
+ - `bracket1` -⠀the opening bracket (e.g. `(`, `{`, `[`, …)
39
+ - `bracket2` -⠀the closing bracket (e.g. `)`, `}`, `]`, …)
40
+ - `is_group` -⠀whether to create a capturing group for the content inside the brackets
41
+ - `strip_spaces` -⠀whether to strip spaces from the bracket content or not
42
+ - `ignore_in_strings` -⠀whether to ignore closing brackets that are inside
43
+ strings/quotes (e.g. `'…)…'` or `"…)…"`)\n
44
+ ---------------------------------------------------------------------------------------
45
+ Attention: Requires non-standard library `regex`, not standard library `re`!"""
46
+ g = "" if is_group else "?:"
47
+ b1 = _rx.escape(bracket1) if len(bracket1) == 1 else bracket1
48
+ b2 = _rx.escape(bracket2) if len(bracket2) == 1 else bracket2
49
+ s1 = r"\s*" if strip_spaces else ""
50
+ s2 = "" if strip_spaces else r"\s*"
51
+
52
+ if ignore_in_strings:
53
+ return cls._clean( \
54
+ rf"""{b1}{s1}({g}{s2}(?:
55
+ [^{b1}{b2}"']
56
+ |"(?:\\.|[^"\\])*"
57
+ |'(?:\\.|[^'\\])*'
58
+ |{b1}(?:
59
+ [^{b1}{b2}"']
60
+ |"(?:\\.|[^"\\])*"
61
+ |'(?:\\.|[^'\\])*'
62
+ |(?R)
63
+ )*{b2}
64
+ )*{s2}){s1}{b2}"""
65
+ )
66
+ else:
67
+ return cls._clean( \
68
+ rf"""{b1}{s1}({g}{s2}(?:
69
+ [^{b1}{b2}]
70
+ |{b1}(?:
71
+ [^{b1}{b2}]
72
+ |(?R)
73
+ )*{b2}
74
+ )*{s2}){s1}{b2}"""
75
+ )
76
+
77
+ @classmethod
78
+ def outside_strings(cls, pattern: str = r".*") -> str:
79
+ """Matches the `pattern` only when it is not found inside a string (`'…'` or `"…"`)."""
80
+ return rf"""(?<!["'])(?:{pattern})(?!["'])"""
81
+
82
+ @classmethod
83
+ def all_except(cls, disallowed_pattern: str, ignore_pattern: str = "", is_group: bool = False) -> str:
84
+ """Matches everything up to the `disallowed_pattern`, unless the
85
+ `disallowed_pattern` is found inside a string/quotes (`'…'` or `"…"`).\n
86
+ -------------------------------------------------------------------------------------
87
+ - `disallowed_pattern` -⠀the pattern that is not allowed to be matched
88
+ - `ignore_pattern` -⠀a pattern that, if found, will make the regex ignore the
89
+ `disallowed_pattern` (even if it contains the `disallowed_pattern` inside it):<br>
90
+ For example if `disallowed_pattern` is `>` and `ignore_pattern` is `->`,
91
+ the `->`-arrows will be allowed, even though they have `>` in them.
92
+ - `is_group` -⠀whether to create a capturing group for the matched content"""
93
+ g = "" if is_group else "?:"
94
+
95
+ return cls._clean( \
96
+ rf"""({g}
97
+ (?:(?!{ignore_pattern}).)*
98
+ (?:(?!{cls.outside_strings(disallowed_pattern)}).)*
99
+ )"""
100
+ )
101
+
102
+ @classmethod
103
+ def func_call(cls, func_name: Optional[str] = None) -> str:
104
+ """Match a function call, and get back two groups:
105
+ 1. function name
106
+ 2. the function's arguments\n
107
+ If no `func_name` is given, it will match any function call.\n
108
+ ---------------------------------------------------------------------------------
109
+ Attention: Requires non-standard library `regex`, not standard library `re`!"""
110
+ if func_name in {"", None}:
111
+ func_name = r"[\w_]+"
112
+
113
+ return rf"""(?<=\b)({func_name})\s*{cls.brackets("(", ")", is_group=True)}"""
114
+
115
+ @classmethod
116
+ def rgba_str(cls, fix_sep: Optional[str] = ",", allow_alpha: bool = True) -> str:
117
+ """Matches an RGBA color inside a string.\n
118
+ ----------------------------------------------------------------------------------
119
+ - `fix_sep` -⠀the fixed separator between the RGBA values (e.g. `,`, `;` …)<br>
120
+ If set to nothing or `None`, any char that is not a letter or number
121
+ can be used to separate the RGBA values, including just a space.
122
+ - `allow_alpha` -⠀whether to include the alpha channel in the match\n
123
+ ----------------------------------------------------------------------------------
124
+ The RGBA color can be in the formats (for `fix_sep = ','`):
125
+ - `rgba(r, g, b)`
126
+ - `rgba(r, g, b, a)` (if `allow_alpha=True`)
127
+ - `(r, g, b)`
128
+ - `(r, g, b, a)` (if `allow_alpha=True`)
129
+ - `r, g, b`
130
+ - `r, g, b, a` (if `allow_alpha=True`)\n
131
+ #### Valid ranges:
132
+ - `r` 0-255 (int: red)
133
+ - `g` 0-255 (int: green)
134
+ - `b` 0-255 (int: blue)
135
+ - `a` 0.0-1.0 (float: opacity)"""
136
+ fix_sep = _re.escape(fix_sep) if isinstance(fix_sep, str) else r"[^0-9A-Z]"
137
+
138
+ rgb_part = rf"""((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
139
+ (?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
140
+ (?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))"""
141
+
142
+ if allow_alpha:
143
+ return cls._clean( \
144
+ rf"""(?ix)(?:rgb|rgba)?\s*(?:
145
+ \(?\s*{rgb_part}
146
+ (?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
147
+ \s*\)?
148
+ )"""
149
+ )
150
+ else:
151
+ return cls._clean( \
152
+ rf"""(?ix)(?:rgb|rgba)?\s*(?:
153
+ \(?\s*{rgb_part}\s*\)?
154
+ )"""
155
+ )
156
+
157
+ @classmethod
158
+ def hsla_str(cls, fix_sep: Optional[str] = ",", allow_alpha: bool = True) -> str:
159
+ """Matches a HSLA color inside a string.\n
160
+ ----------------------------------------------------------------------------------
161
+ - `fix_sep` -⠀the fixed separator between the HSLA values (e.g. `,`, `;` …)<br>
162
+ If set to nothing or `None`, any char that is not a letter or number
163
+ can be used to separate the HSLA values, including just a space.
164
+ - `allow_alpha` -⠀whether to include the alpha channel in the match\n
165
+ ----------------------------------------------------------------------------------
166
+ The HSLA color can be in the formats (for `fix_sep = ','`):
167
+ - `hsla(h, s, l)`
168
+ - `hsla(h, s, l, a)` (if `allow_alpha=True`)
169
+ - `(h, s, l)`
170
+ - `(h, s, l, a)` (if `allow_alpha=True`)
171
+ - `h, s, l`
172
+ - `h, s, l, a` (if `allow_alpha=True`)\n
173
+ #### Valid ranges:
174
+ - `h` 0-360 (int: hue)
175
+ - `s` 0-100 (int: saturation)
176
+ - `l` 0-100 (int: lightness)
177
+ - `a` 0.0-1.0 (float: opacity)"""
178
+ fix_sep = _re.escape(fix_sep) if isinstance(fix_sep, str) else r"[^0-9A-Z]"
179
+
180
+ hsl_part = rf"""((?:0*(?:360|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])))(?:\s*°)?
181
+ (?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))(?:\s*%)?
182
+ (?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))(?:\s*%)?"""
183
+
184
+ if allow_alpha:
185
+ return cls._clean( \
186
+ rf"""(?ix)(?:hsl|hsla)?\s*(?:
187
+ \(?\s*{hsl_part}
188
+ (?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
189
+ \s*\)?
190
+ )"""
191
+ )
192
+ else:
193
+ return cls._clean( \
194
+ rf"""(?ix)(?:hsl|hsla)?\s*(?:
195
+ \(?\s*{hsl_part}\s*\)?
196
+ )"""
197
+ )
198
+
199
+ @classmethod
200
+ def hexa_str(cls, allow_alpha: bool = True) -> str:
201
+ """Matches a HEXA color inside a string.\n
202
+ ----------------------------------------------------------------------
203
+ - `allow_alpha` -⠀whether to include the alpha channel in the match\n
204
+ ----------------------------------------------------------------------
205
+ The HEXA color can be in the formats (prefix `#`, `0x` or no prefix):
206
+ - `RGB`
207
+ - `RGBA` (if `allow_alpha=True`)
208
+ - `RRGGBB`
209
+ - `RRGGBBAA` (if `allow_alpha=True`)\n
210
+ #### Valid ranges:
211
+ every channel from 0-9 and A-F (case insensitive)"""
212
+ return r"(?i)(?:#|0x)?([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3})" \
213
+ if allow_alpha else r"(?i)(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})"
214
+
215
+ @classmethod
216
+ def _clean(cls, pattern: str) -> str:
217
+ """Internal method to make a multiline-string regex pattern into a single-line pattern."""
218
+ return "".join(line.strip() for line in pattern.splitlines()).strip()
219
+
220
+
221
+ @mypyc_attr(native_class=False)
222
+ class LazyRegex:
223
+ """A class that lazily compiles and caches regex patterns on first access.\n
224
+ --------------------------------------------------------------------------------
225
+ - `**patterns` -⠀keyword arguments where the key is the name of the pattern and
226
+ the value is the regex pattern string to compile\n
227
+ --------------------------------------------------------------------------------
228
+ #### Example usage:
229
+ ```python
230
+ PATTERNS = LazyRegex(
231
+ email=r"(?i)[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}",
232
+ phone=r"\\+?\\d{1,3}[-.\\s]?\\(?\\d{1,4}\\)?[-.\\s]?\\d{1,4}[-.\\s]?\\d{1,9}",
233
+ )
234
+
235
+ email_pattern = PATTERNS.email # Compiles and caches the EMAIL pattern
236
+ phone_pattern = PATTERNS.phone # Compiles and caches the PHONE pattern
237
+ ```"""
238
+
239
+ def __init__(self, **patterns: str):
240
+ self._patterns = patterns
241
+
242
+ def __getattr__(self, name: str) -> _rx.Pattern:
243
+ if name in self._patterns:
244
+ setattr(self, name, compiled := _rx.compile(self._patterns[name]))
245
+ return compiled
246
+
247
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
Binary file
xulbux/string.py ADDED
@@ -0,0 +1,161 @@
1
+ """
2
+ This module provides the `String` class, which includes
3
+ various utility methods for string manipulation and conversion.
4
+ """
5
+
6
+ from typing import Optional, Literal, Any
7
+ import json as _json
8
+ import ast as _ast
9
+ import re as _re
10
+
11
+
12
+ class String:
13
+ """This class provides various utility methods for string manipulation and conversion."""
14
+
15
+ @classmethod
16
+ def to_type(cls, string: str) -> Any:
17
+ """Will convert a string to the found type, including complex nested structures.\n
18
+ -----------------------------------------------------------------------------------
19
+ - `string` -⠀the string to convert"""
20
+ try:
21
+ return _ast.literal_eval(string := string.strip())
22
+ except (ValueError, SyntaxError):
23
+ try:
24
+ return _json.loads(string)
25
+ except _json.JSONDecodeError:
26
+ return string
27
+
28
+ @classmethod
29
+ def normalize_spaces(cls, string: str, tab_spaces: int = 4) -> str:
30
+ """Replaces all special space characters with normal spaces.\n
31
+ ---------------------------------------------------------------
32
+ - `tab_spaces` -⠀number of spaces to replace tab chars with"""
33
+ if tab_spaces < 0:
34
+ raise ValueError(f"The 'tab_spaces' parameter must be non-negative, got {tab_spaces!r}")
35
+
36
+ return string.replace("\t", " " * tab_spaces).replace("\u2000", " ").replace("\u2001", " ").replace("\u2002", " ") \
37
+ .replace("\u2003", " ").replace("\u2004", " ").replace("\u2005", " ").replace("\u2006", " ") \
38
+ .replace("\u2007", " ").replace("\u2008", " ").replace("\u2009", " ").replace("\u200A", " ")
39
+
40
+ @classmethod
41
+ def escape(cls, string: str, str_quotes: Optional[Literal["'", '"']] = None) -> str:
42
+ """Escapes Python's special characters (e.g. `\\n`, `\\t`, …) and quotes inside the string.\n
43
+ --------------------------------------------------------------------------------------------------------
44
+ - `string` -⠀the string to escape
45
+ - `str_quotes` -⠀the type of quotes the string will be put inside of (or None to not escape quotes)<br>
46
+ Can be either `"` or `'` and should match the quotes, the string will be put inside of.<br>
47
+ So if your string will be `"string"`, `str_quotes` should be `"`.<br>
48
+ That way, if the string includes the same quotes, they will be escaped."""
49
+ string = string.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") \
50
+ .replace("\b", "\\b").replace("\f", "\\f").replace("\a", "\\a")
51
+
52
+ if str_quotes == '"':
53
+ return string.replace("\\'", "'").replace('"', '\\"')
54
+ elif str_quotes == "'":
55
+ return string.replace('\\"', '"').replace("'", "\\'")
56
+ else:
57
+ return string
58
+
59
+ @classmethod
60
+ def is_empty(cls, string: Optional[str], spaces_are_empty: bool = False) -> bool:
61
+ """Returns `True` if the string is considered empty and `False` otherwise.\n
62
+ -----------------------------------------------------------------------------------------------
63
+ - `string` -⠀the string to check (or `None`, which is considered empty)
64
+ - `spaces_are_empty` -⠀if true, strings consisting only of spaces are also considered empty"""
65
+ return bool(
66
+ (string in {"", None}) or \
67
+ (spaces_are_empty and isinstance(string, str) and not string.strip())
68
+ )
69
+
70
+ @classmethod
71
+ def single_char_repeats(cls, string: str, char: str) -> int | bool:
72
+ """- If the string consists of only the same `char`, it returns the number of times it is present.
73
+ - If the string doesn't consist of only the same character, it returns `False`.\n
74
+ ---------------------------------------------------------------------------------------------------
75
+ - `string` -⠀the string to check
76
+ - `char` -⠀the character to check for repetition"""
77
+ if len(char) != 1:
78
+ raise ValueError(f"The 'char' parameter must be a single character, got {char!r}")
79
+
80
+ if len(string) == (len(char) * string.count(char)):
81
+ return string.count(char)
82
+ else:
83
+ return False
84
+
85
+ @classmethod
86
+ def decompose(cls, case_string: str, seps: str = "-_", lower_all: bool = True) -> list[str]:
87
+ """Will decompose the string (any type of casing, also mixed) into parts.\n
88
+ ----------------------------------------------------------------------------
89
+ - `case_string` -⠀the string to decompose
90
+ - `seps` -⠀additional separators to split the string at
91
+ - `lower_all` -⠀if true, all parts will be converted to lowercase"""
92
+ return [
93
+ (part.lower() if lower_all else part) \
94
+ for part in _re.split(rf"(?<=[a-z])(?=[A-Z])|[{_re.escape(seps)}]", case_string)
95
+ ]
96
+
97
+ @classmethod
98
+ def to_camel_case(cls, string: str, upper: bool = True) -> str:
99
+ """Will convert the string of any type of casing to CamelCase.\n
100
+ -----------------------------------------------------------------
101
+ - `string` -⠀the string to convert
102
+ - `upper` -⠀if true, it will convert to UpperCamelCase,
103
+ otherwise to lowerCamelCase"""
104
+ parts = cls.decompose(string)
105
+
106
+ return (
107
+ ("" if upper else parts[0].lower()) + \
108
+ "".join(part.capitalize() for part in (parts if upper else parts[1:]))
109
+ )
110
+
111
+ @classmethod
112
+ def to_delimited_case(cls, string: str, delimiter: str = "_", screaming: bool = False) -> str:
113
+ """Will convert the string of any type of casing to delimited case.\n
114
+ -----------------------------------------------------------------------
115
+ - `string` -⠀the string to convert
116
+ - `delimiter` -⠀the delimiter to use between parts
117
+ - `screaming` -⠀whether to convert all parts to uppercase"""
118
+ return delimiter.join(
119
+ part.upper() if screaming else part \
120
+ for part in cls.decompose(string)
121
+ )
122
+
123
+ @classmethod
124
+ def get_lines(cls, string: str, remove_empty_lines: bool = False) -> list[str]:
125
+ """Will split the string into lines.\n
126
+ ------------------------------------------------------------------------------------
127
+ - `string` -⠀the string to split
128
+ - `remove_empty_lines` -⠀if true, it will remove all empty lines from the result"""
129
+ if not remove_empty_lines:
130
+ return string.splitlines()
131
+ elif not (lines := string.splitlines()):
132
+ return []
133
+ elif not (non_empty_lines := [line for line in lines if line.strip()]):
134
+ return []
135
+ else:
136
+ return non_empty_lines
137
+
138
+ @classmethod
139
+ def remove_consecutive_empty_lines(cls, string: str, max_consecutive: int = 0) -> str:
140
+ """Will remove consecutive empty lines from the string.\n
141
+ -------------------------------------------------------------------------------------
142
+ - `string` -⠀the string to process
143
+ - `max_consecutive` -⠀the maximum number of allowed consecutive empty lines.<br>
144
+ * If `0`, it will remove all consecutive empty lines.
145
+ * If bigger than `0`, it will only allow `max_consecutive` consecutive empty lines
146
+ and everything above it will be cut down to `max_consecutive` empty lines."""
147
+ if max_consecutive < 0:
148
+ raise ValueError(f"The 'max_consecutive' parameter must be non-negative, got {max_consecutive!r}")
149
+
150
+ return _re.sub(r"(\n\s*){2,}", r"\1" * (max_consecutive + 1), string)
151
+
152
+ @classmethod
153
+ def split_count(cls, string: str, count: int) -> list[str]:
154
+ """Will split the string every `count` characters.\n
155
+ -----------------------------------------------------
156
+ - `string` -⠀the string to split
157
+ - `count` -⠀the number of characters per part"""
158
+ if count <= 0:
159
+ raise ValueError(f"The 'count' parameter must be a positive integer, got {count!r}")
160
+
161
+ return [string[i:i + count] for i in range(0, len(string), count)]
Binary file