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/__init__.py +12 -12
- xulbux/_cli_.py +13 -12
- xulbux/_consts_.py +1 -0
- xulbux/xx_code.py +6 -6
- xulbux/xx_color.py +73 -66
- xulbux/{xx_cmd.py → xx_console.py} +39 -39
- xulbux/xx_data.py +241 -268
- xulbux/xx_env_path.py +113 -0
- xulbux/xx_file.py +25 -25
- xulbux/xx_format_codes.py +55 -36
- xulbux/xx_json.py +4 -4
- xulbux/xx_regex.py +29 -14
- xulbux/xx_string.py +89 -72
- xulbux-1.5.9.dist-info/METADATA +110 -0
- xulbux-1.5.9.dist-info/RECORD +21 -0
- {xulbux-1.5.7.dist-info → xulbux-1.5.9.dist-info}/WHEEL +2 -1
- xulbux-1.5.9.dist-info/entry_points.txt +3 -0
- xulbux-1.5.9.dist-info/top_level.txt +1 -0
- xulbux/xx_env_vars.py +0 -73
- xulbux-1.5.7.dist-info/METADATA +0 -101
- xulbux-1.5.7.dist-info/RECORD +0 -20
- xulbux-1.5.7.dist-info/entry_points.txt +0 -3
- {xulbux-1.5.7.dist-info/licenses → xulbux-1.5.9.dist-info}/LICENSE +0 -0
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
|
-
|
|
141
|
-
|
|
143
|
+
formats = match.group(1)
|
|
144
|
+
escaped = match.group(2)
|
|
142
145
|
auto_reset_txt = match.group(3)
|
|
143
|
-
if
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
|
198
|
+
if escaped and auto_reset_txt
|
|
181
199
|
else auto_reset_txt if auto_reset_txt else ""
|
|
182
200
|
)
|
|
183
|
-
+ ("" if
|
|
201
|
+
+ ("" if escaped else "".join(ansi_resets))
|
|
184
202
|
)
|
|
185
203
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
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.
|
|
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
|
|
56
|
-
If `is_group` is `True`, you will be able to reference the matched
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
13
|
+
# NONE
|
|
14
|
+
elif _re.match(r"(?i)^(none|null|undefined)$", string):
|
|
12
15
|
return None
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
elif
|
|
18
|
-
return
|
|
19
|
-
|
|
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
|
|
45
|
+
for k, v in _re.findall(
|
|
46
|
+
r"((?:[^:,{}]+|\[.*?\]|\(.*?\)|\{.*?\})+)\s*:\s*((?:[^:,{}]+|\[.*?\]|\(.*?\)|\{.*?\})+)", string[1:-1]
|
|
47
|
+
)
|
|
23
48
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 [
|
|
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
|
|
76
|
-
return
|
|
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
|
|
80
|
-
"""Will convert the string of any type of casing to
|
|
81
|
-
return
|
|
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
|
|
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
|