xulbux 1.5.5__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.
@@ -0,0 +1,212 @@
1
+ """
2
+ Functions to be able to use special (easy) formatting codes directly inside some message (string).<br>
3
+ These codes, when used within following functions, will change the look of log within the console:
4
+ - `FormatCodes.print()` (*print a special format-codes containing string*)
5
+ - `FormatCodes.input()` (*input with a special format-codes containing prompt*)
6
+ - `FormatCodes.to_ansi()` (*transform all special format-codes into ANSI codes in a string*)\n
7
+ --------------------------------------------------------------------------------------------------------------------
8
+ How to change the text format and color?<br>
9
+ **Example string with formatting codes:**<br>
10
+ > `[bold]This is bold text, [#F08]which is pink now [black|BG:#FF0088] and now it changed`<br>
11
+ > `to black with a pink background. [_]And this is the boring text, where everything is reset.`\n
12
+ ⇾ **Instead of writing the formats all separate** `[…][…][…]` **you can join them like this** `[…|…|…]`\n
13
+ --------------------------------------------------------------------------------------------------------------------
14
+ You can also automatically reset a certain format, behind text like shown in the following example:<br>
15
+ > `This is normal text [b](which is bold now) but now it was automatically reset to normal.`\n
16
+ This will only reset formats, that have a reset listed below. Colors and BG-colors won't be reset.<br>
17
+ This is what will happen, if you use it with a color-format:<br>
18
+ > `[cyan]This is cyan text [b](which is bold now.) Now it's not bold any more but still cyan.`\n
19
+ If you want to ignore the `()` brackets you can put a `\\` or `/` between:<br>
20
+ > `[cyan]This is cyan text [b]/(which is bold now.) And now it is still bold and cyan.`\n
21
+ ⇾ **To see these examples in action, you can put them into the** `FormatCodes.print()` **function.**\n
22
+ --------------------------------------------------------------------------------------------------------------------
23
+ **All possible formatting codes:**
24
+ - HEX colors: `[#F08]` or `[#FF0088]` (*with or without leading #*)
25
+ - RGB colors: `[rgb(255, 0, 136)]`
26
+ - bright colors: `[bright:#F08]`
27
+ - background colors: `[BG:#F08]`
28
+ - standard cmd colors:
29
+ - `[black]`
30
+ - `[red]`
31
+ - `[green]`
32
+ - `[yellow]`
33
+ - `[blue]`
34
+ - `[magenta]`
35
+ - `[cyan]`
36
+ - `[white]`
37
+ - bright cmd colors: `[bright:black]` or `[br:black]`, `[bright:red]` or `[br:red]`, ...
38
+ - background cmd colors: `[BG:black]`, `[BG:red]`, ...
39
+ - bright background cmd colors: `[BG:bright:black]` or `[BG:br:black]`, `[BG:bright:red]` or `[BG:br:red]`, ...<br>
40
+ ⇾ **The order of** `BG:` **and** `bright:` or `br:` **does not matter.**
41
+ - text formats:
42
+ - `[bold]` or `[b]`
43
+ - `[dim]`
44
+ - `[italic]` or `[i]`
45
+ - `[underline]` or `[u]`
46
+ - `[inverse]`, `[invert]` or `[in]`
47
+ - `[hidden]`, `[hide]` or `[h]`
48
+ - `[strikethrough]` or `[s]`
49
+ - `[double-underline]` or `[du]`
50
+ - specific reset: `[_bold]` or `[_b]`, `[_dim]`, ... or `[_color]` or `[_c]`, `[_background]` or `[_bg]`
51
+ - total reset: `[_]` (only if no `default_color` is set, otherwise see **↓** )
52
+ --------------------------------------------------------------------------------------------------------------------
53
+ **Special formatting when param `default_color` is set to a color:**
54
+ - `[*]` will reset everything, just like `[_]`, but the text-color will remain in `default_color`
55
+ - `[*color]` will reset the text-color, just like `[_color]`, but then also make it `default_color`
56
+ - `[default]` will just color the text in `default_color`,
57
+ - `[BG:default]` will color the background in `default_color`\n
58
+ Unlike the standard cmd colors, the default color can be changed by using the following modifiers:
59
+ - `[l]` will lighten the `default_color` text by `brightness_steps`%
60
+ - `[ll]` will lighten the `default_color` text by `2 × brightness_steps`%
61
+ - `[lll]` will lighten the `default_color` text by `3 × brightness_steps`%
62
+ - ... etc. Same thing for darkening:
63
+ - `[d]` will darken the `default_color` text by `brightness_steps`%
64
+ - `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
65
+ - `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
66
+ - ... etc.\n
67
+ Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.<br>
68
+ This can also be changed by changing the param `_modifiers = ('+l', '-d')`.
69
+ """
70
+
71
+
72
+ from ._consts_ import ANSI
73
+ from .xx_string import *
74
+ from .xx_regex import *
75
+ from .xx_color import *
76
+ from .xx_data import *
77
+
78
+ import ctypes as _ctypes
79
+ import regex as _rx
80
+ import sys as _sys
81
+ import re as _re
82
+
83
+
84
+
85
+
86
+ class FormatCodes:
87
+
88
+ @staticmethod
89
+ def print(*values:object, default_color:hexa|rgba = None, brightness_steps:int = 20, sep:str = ' ', end:str = '\n') -> None:
90
+ FormatCodes.__config_console()
91
+ _sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)), default_color, brightness_steps) + end)
92
+ _sys.stdout.flush()
93
+
94
+ @staticmethod
95
+ def input(prompt:object = '', default_color:hexa|rgba = None, brightness_steps:int = 20) -> str:
96
+ FormatCodes.__config_console()
97
+ return input(FormatCodes.to_ansi(prompt, default_color, brightness_steps))
98
+
99
+ @staticmethod
100
+ def to_ansi(string:str, default_color:hexa|rgba = None, brightness_steps:int = 20, _default_start:bool = True) -> str:
101
+ result, use_default = '', default_color and (Color.is_valid_rgba(default_color, False) or Color.is_valid_hexa(default_color, False))
102
+ if use_default:
103
+ string = _re.sub(r'\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]', r'[\1_|default\2]', string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
104
+ string = _re.sub(r'\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]', r'[\1default\2]', string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
105
+ def replace_keys(match:_rx.Match) -> str:
106
+ format_keys, esc, auto_reset_txt = match.group(1), match.group(2), match.group(3)
107
+ if not format_keys:
108
+ return match.group(0)
109
+ else:
110
+ format_keys = [k.replace(' ', '') for k in format_keys.split('|') if k.replace(' ', '')]
111
+ ansi_resets, ansi_formats = [], [FormatCodes.__get_replacement(k, default_color, brightness_steps) for k in format_keys]
112
+ if auto_reset_txt and not esc:
113
+ reset_keys = ['_color' if Color.is_valid(k) or k in ANSI.color_map
114
+ else '_bg' if (set(k.lower().split(':')) & {'bg', 'bright', 'br'} and len(k.split(':')) <= 3 and any(Color.is_valid(k[i:]) or k[i:] in ANSI.color_map for i in range(len(k))))
115
+ else f'_{k}' for k in format_keys]
116
+ ansi_resets = [r for k in reset_keys if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(f'{ANSI.char}{ANSI.start}')]
117
+ if not all(f.startswith(f'{ANSI.char}{ANSI.start}') for f in ansi_formats): return match.group(0)
118
+ return ''.join(ansi_formats) + ((f'({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})' if esc else auto_reset_txt) if auto_reset_txt else '') + ('' if esc else ''.join(ansi_resets))
119
+ result = '\n'.join(_rx.sub(Regex.brackets('[', ']', is_group=True) + r'(?:\s*([/\\]?)\s*' + Regex.brackets('(', ')', is_group=True) + r')?', replace_keys, line) for line in string.splitlines())
120
+ return (FormatCodes.__get_default_ansi(default_color) if _default_start else '') + result if use_default else result
121
+
122
+ @staticmethod
123
+ def __config_console() -> None:
124
+ _sys.stdout.flush()
125
+ kernel32 = _ctypes.windll.kernel32
126
+ h = kernel32.GetStdHandle(-11)
127
+ mode = _ctypes.c_ulong()
128
+ kernel32.GetConsoleMode(h, _ctypes.byref(mode))
129
+ kernel32.SetConsoleMode(h, mode.value | 0x0004) # ENABLE VIRTUAL TERMINAL PROCESSING
130
+
131
+ @staticmethod
132
+ def __get_default_ansi(default_color:hexa|rgba, format_key:str = None, brightness_steps:int = None, _modifiers:tuple[str,str] = ('+l', '-d')) -> str|None:
133
+ if Color.is_valid_hexa(default_color, False):
134
+ default_color = Color.to_rgba(default_color)
135
+ if not brightness_steps or (format_key and _re.search(r'(?i)((?:BG\s*:)?)\s*default', format_key)):
136
+ if format_key and _re.search(r'(?i)BG\s*:\s*default', format_key):
137
+ return ANSI.seq_bg_color.format(default_color[0], default_color[1], default_color[2])
138
+ return ANSI.seq_color.format(default_color[0], default_color[1], default_color[2])
139
+ match = _re.match(rf'(?i)((?:BG\s*:)?)\s*({"|".join([f"{_re.escape(m)}+" for m in _modifiers[0] + _modifiers[1]])})$', format_key)
140
+ if not match or not match.group(2):
141
+ return None
142
+ is_bg, modifier = match.group(1), match.group(2)
143
+ new_rgb, lighten, darken = None, None, None
144
+ for mod in _modifiers[0]:
145
+ lighten = String.get_repeated_symbol(modifier, mod)
146
+ if lighten and lighten > 0:
147
+ new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * lighten)
148
+ break
149
+ if not new_rgb:
150
+ for mod in _modifiers[1]:
151
+ darken = String.get_repeated_symbol(modifier, mod)
152
+ if darken and darken > 0:
153
+ new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * darken)
154
+ break
155
+ if new_rgb:
156
+ return ANSI.seq_bg_color.format(new_rgb[0], new_rgb[1], new_rgb[2]) if is_bg else ANSI.seq_color.format(new_rgb[0], new_rgb[1], new_rgb[2])
157
+
158
+ @staticmethod
159
+ def __get_replacement(format_key:str, default_color:hexa|rgba = None, brightness_steps:int = 20, _modifiers:tuple[str, str] = ('+l', '-d')) -> str:
160
+ """Gives you the corresponding ANSI code for the given format key.<br>
161
+ If `default_color` is not `None`, the text color will be `default_color` if all formats<br>
162
+ are reset or you can get lighter or darker version of `default_color` (also as BG) by<br>
163
+ using one or more `_modifiers` symbols as a format key ()"""
164
+ def key_exists(key:str) -> bool:
165
+ for map_key in ANSI.codes_map:
166
+ if isinstance(map_key, tuple) and key in map_key:
167
+ return True
168
+ elif key == map_key:
169
+ return True
170
+ return False
171
+ def get_value(key:str) -> any:
172
+ for map_key in ANSI.codes_map:
173
+ if isinstance(map_key, tuple) and key in map_key:
174
+ return ANSI.codes_map[map_key]
175
+ elif key == map_key:
176
+ return ANSI.codes_map[map_key]
177
+ return None
178
+ use_default = default_color and (Color.is_valid_rgba(default_color, False) or Color.is_valid_hexa(default_color, False))
179
+ _format_key, format_key = format_key, FormatCodes.__normalize(format_key)
180
+ if use_default:
181
+ new_default_color = FormatCodes.__get_default_ansi(default_color, format_key, brightness_steps, _modifiers)
182
+ if new_default_color:
183
+ return new_default_color
184
+ if key_exists(format_key):
185
+ return ANSI.seq().format(get_value(format_key))
186
+ rgb_match = _re.match(r'(?i)\s*(BG\s*:)?\s*(?:rgb|rgba)?\s*\(?\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)?\s*', format_key)
187
+ hex_match = _re.match(r'(?i)\s*(BG\s*:)?\s*(?:#|0x)?([0-9A-F]{8}|[0-9A-F]{6}|[0-9A-F]{4}|[0-9A-F]{3})\s*', format_key)
188
+ try:
189
+ if rgb_match:
190
+ is_bg = rgb_match.group(1)
191
+ r, g, b = map(int, rgb_match.groups()[1:])
192
+ if Color.is_valid_rgba((r, g, b)):
193
+ return ANSI.seq_bg_color.format(r, g, b) if is_bg else ANSI.seq_color.format(r, g, b)
194
+ elif hex_match:
195
+ is_bg = hex_match.group(1)
196
+ rgb = Color.to_rgba(hex_match.group(2))
197
+ return ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2]) if is_bg else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
198
+ except Exception: pass
199
+ return _format_key
200
+
201
+ @staticmethod
202
+ def __normalize(format_key:str) -> str:
203
+ """Put the given format key in the correct format:<br>
204
+ `1` put `BG:` as first key-part<br>
205
+ `2` put `bright:` or `br:` as second key-part<br>
206
+ `3` put everything else behind<br>
207
+ `4` everything in lower case"""
208
+ format_key = format_key.replace(' ', '').lower().strip()
209
+ if ':' in format_key:
210
+ key_parts = format_key.split(':')
211
+ format_key = ('bg:' if 'bg' in key_parts else '') + ('bright:' if 'bright' in key_parts or 'br' in key_parts else '') + ''.join(Data.remove(key_parts, ['bg', 'bright', 'br']))
212
+ return format_key
xulbux/xx_json.py ADDED
@@ -0,0 +1,81 @@
1
+ from .xx_data import *
2
+ from .xx_file import *
3
+
4
+ import json as _json
5
+ import os as _os
6
+
7
+
8
+
9
+
10
+ class Json:
11
+
12
+ @staticmethod
13
+ def read(json_file:str, comment_start:str = '>>', comment_end:str = '<<', return_original:bool = False) -> dict|tuple[dict,dict]:
14
+ """Read JSON files, ignoring comments.\n
15
+ -------------------------------------------------------------------------
16
+ If only `comment_start` is found at the beginning of an item,<br>
17
+ the whole item is counted as a comment and therefore ignored.<br>
18
+ If `comment_start` and `comment_end` are found inside an item,<br>
19
+ the the section from `comment_start` to `comment_end` is ignored.<br>
20
+ If `return_original` is set to `True`, the original JSON is returned<br>
21
+ additionally. (returns: `[processed_json, original_json]`)"""
22
+ file_path = File._make_path(json_file, 'json', prefer_base_dir=True)
23
+ with open(file_path, 'r') as f:
24
+ content = f.read()
25
+ try:
26
+ data = _json.loads(content)
27
+ except _json.JSONDecodeError as e:
28
+ raise ValueError(f"Error parsing JSON in '{file_path}': {str(e)}")
29
+ processed_data = Data.remove_comments(data, comment_start, comment_end)
30
+ if not processed_data:
31
+ raise ValueError(f"The JSON file '{file_path}' is empty or contains only comments.")
32
+ return (processed_data, data) if return_original else processed_data
33
+
34
+ @staticmethod
35
+ def create(content:dict, new_file:str = 'config', indent:int = 2, compactness:int = 1, force:bool = False) -> str:
36
+ file_path = File._make_path(new_file, 'json', prefer_base_dir=True)
37
+ if _os.path.exists(file_path) and not force:
38
+ with open(file_path, 'r', encoding='utf-8') as existing_f:
39
+ existing_content = _json.load(existing_f)
40
+ if existing_content == content:
41
+ raise FileExistsError(f'Already created this file. (nothing changed)')
42
+ raise FileExistsError('File already exists.')
43
+ with open(file_path, 'w', encoding='utf-8') as f:
44
+ f.write(Data.to_str(content, indent, compactness, as_json=True))
45
+ full_path = _os.path.abspath(file_path)
46
+ return full_path
47
+
48
+ @staticmethod
49
+ def update(json_file:str, update_values:str|list[str], comment_start:str = '>>', comment_end:str = '<<', sep:tuple[str,str] = ('->', '::')) -> None:
50
+ """Function to easily update single/multiple values inside JSON files.\n
51
+ --------------------------------------------------------------------------------------------------------
52
+ The param `json_file` is the path to the JSON file or just the name of the JSON file to be updated.\n
53
+ --------------------------------------------------------------------------------------------------------
54
+ The param `update_values` is a sort of path (or a list of paths) to the value/s to be updated, with<br>
55
+ the new value at the end of the path.<br>
56
+ In this example:
57
+ ```\n {
58
+ 'healthy': {
59
+ 'fruit': ['apples', 'bananas', 'oranges'],
60
+ 'vegetables': ['carrots', 'broccoli', 'celery']
61
+ }
62
+ }\n```
63
+ ... if you want to change the value of `'apples'` to `'strawberries'`, `update_values` would<br>
64
+ be `healthy->fruit->apples::strawberries` or if you don't know that the value to update<br>
65
+ is `apples` you can also use the position of the value, so `healthy->fruit->0::strawberries`.\n
66
+ ⇾ **If the path from `update_values` doesn't exist, it will be created.**\n
67
+ --------------------------------------------------------------------------------------------------------
68
+ If only `comment_start` is found at the beginning of an item, the whole item is counted<br>
69
+ as a comment and therefore ignored. If `comment_start` and `comment_end` are found<br>
70
+ inside an item, the the section from `comment_start` to `comment_end` is ignored."""
71
+ if isinstance(update_values, str):
72
+ update_values = [update_values]
73
+ valid_entries = [(parts[0].strip(), parts[1]) for update_value in update_values if len(parts := update_value.split(str(sep[1]).strip())) == 2]
74
+ value_paths, new_values = (zip(*valid_entries) if valid_entries else ([], []))
75
+ processed_data, data = Json.read(json_file, comment_start, comment_end, return_original=True)
76
+ update = []
77
+ for value_path, new_value in zip(value_paths, new_values):
78
+ path_id = Data.get_path_id(processed_data, value_path)
79
+ update.append(f'{path_id}::{new_value}')
80
+ updated = Data.set_value_by_path_id(data, update)
81
+ Json.create(updated, json_file, force=True)
xulbux/xx_path.py ADDED
@@ -0,0 +1,97 @@
1
+ import tempfile as _tempfile
2
+ import difflib as _difflib
3
+ import shutil as _shutil
4
+ import sys as _sys
5
+ import os as _os
6
+
7
+
8
+
9
+
10
+ class Path:
11
+
12
+ @staticmethod
13
+ def get(cwd:bool = False, base_dir:bool = False) -> str|list:
14
+ paths = []
15
+ if cwd:
16
+ paths.append(_os.getcwd())
17
+ if base_dir:
18
+ if getattr(_sys, 'frozen', False):
19
+ base_path = _os.path.dirname(_sys.executable)
20
+ else:
21
+ main_module = _sys.modules['__main__']
22
+ if hasattr(main_module, '__file__'):
23
+ base_path = _os.path.dirname(_os.path.abspath(main_module.__file__))
24
+ elif hasattr(main_module, '__spec__') and main_module.__spec__ and getattr(main_module.__spec__, 'origin', None):
25
+ base_path = _os.path.dirname(_os.path.abspath(main_module.__spec__.origin))
26
+ else:
27
+ raise RuntimeError('Can only get base directory if ran from a file.')
28
+ paths.append(base_path)
29
+ return paths[0] if len(paths) == 1 else paths
30
+
31
+ @staticmethod
32
+ def extend(path:str, search_in:str|list[str] = None, raise_error:bool = False, correct_path:bool = False) -> str:
33
+ if path in (None, ''):
34
+ return path
35
+ def get_closest_match(dir:str, part:str) -> str|None:
36
+ try:
37
+ files_and_dirs = _os.listdir(dir)
38
+ matches = _difflib.get_close_matches(part, files_and_dirs, n=1, cutoff=0.6)
39
+ return matches[0] if matches else None
40
+ except:
41
+ return None
42
+ def find_path(start:str, parts:list[str]) -> str|None:
43
+ current = start
44
+ for part in parts:
45
+ if _os.path.isfile(current):
46
+ return current
47
+ closest_match = get_closest_match(current, part) if correct_path else part
48
+ current = _os.path.join(current, closest_match) if closest_match else None
49
+ if current is None:
50
+ return None
51
+ return current if _os.path.exists(current) and current != start else None
52
+ def expand_env_path(p:str) -> str:
53
+ if not '%' in p:
54
+ return p
55
+ parts = p.split('%')
56
+ for i in range(1, len(parts), 2):
57
+ if parts[i].upper() in _os.environ:
58
+ parts[i] = _os.environ[parts[i].upper()]
59
+ return ''.join(parts)
60
+ path = _os.path.normpath(expand_env_path(path))
61
+ if _os.path.isabs(path):
62
+ drive, rel_path = _os.path.splitdrive(path)
63
+ rel_path = rel_path.lstrip(_os.sep)
64
+ search_dirs = [drive + _os.sep] if drive else [_os.sep]
65
+ else:
66
+ rel_path = path.lstrip(_os.sep)
67
+ base_dir = Path.get(base_dir=True)
68
+ search_dirs = [_os.getcwd(), base_dir, _os.path.expanduser('~'), _tempfile.gettempdir()]
69
+ if search_in:
70
+ search_dirs.extend([search_in] if isinstance(search_in, str) else search_in)
71
+ path_parts = rel_path.split(_os.sep)
72
+ for search_dir in search_dirs:
73
+ full_path = _os.path.join(search_dir, rel_path)
74
+ if _os.path.exists(full_path):
75
+ return full_path
76
+ match = find_path(search_dir, path_parts) if correct_path else None
77
+ if match: return match
78
+ if raise_error:
79
+ raise FileNotFoundError(f'Path \'{path}\' not found in specified directories.')
80
+ return _os.path.join(search_dirs[0], rel_path)
81
+
82
+ @staticmethod
83
+ def remove(path:str, only_content:bool = False) -> None:
84
+ if not _os.path.exists(path):
85
+ return None
86
+ if not only_content:
87
+ _shutil.rmtree(path)
88
+ elif _os.path.isdir(path):
89
+ for filename in _os.listdir(path):
90
+ file_path = _os.path.join(path, filename)
91
+ try:
92
+ if _os.path.isfile(file_path) or _os.path.islink(file_path):
93
+ _os.unlink(file_path)
94
+ elif _os.path.isdir(file_path):
95
+ _shutil.rmtree(file_path)
96
+ except Exception as e:
97
+ raise Exception(f'Failed to delete {file_path}. Reason: {e}')
xulbux/xx_regex.py ADDED
@@ -0,0 +1,124 @@
1
+ """
2
+ Really long regex code presets:<br>
3
+ `quotes` match everything inside quotes<br>
4
+ `brackets` match everything inside brackets<br>
5
+ `outside_strings` match the pattern but not inside strings<br>
6
+ `all_except` match everything except a certain pattern<br>
7
+ `func_call` match a function call<br>
8
+ `rgba_str` match an RGBA color<br>
9
+ `hsla_str` match a HSLA color
10
+ """
11
+
12
+
13
+ import re as _re
14
+
15
+
16
+
17
+
18
+ class Regex:
19
+
20
+ @staticmethod
21
+ def quotes() -> str:
22
+ """Matches everything inside quotes. (Strings)\n
23
+ ------------------------------------------------------------------------------------
24
+ Will create two named groups:<br>
25
+ **`quote`** the quote type (single or double)<br>
26
+ **`string`** everything inside the found quote pair\n
27
+ ------------------------------------------------------------------------------------
28
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
29
+ return r'(?P<quote>[\'"])(?P<string>(?:\\.|(?!\g<quote>).)*?)\g<quote>'
30
+
31
+ @staticmethod
32
+ def brackets(bracket1:str = '(', bracket2:str = ')', is_group:bool = False) -> str:
33
+ """Matches everything inside brackets, including other nested brackets.\n
34
+ ------------------------------------------------------------------------------------
35
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
36
+ g, b1, b2 = '' if is_group else '?:', _re.escape(bracket1) if len(bracket1) == 1 else bracket1, _re.escape(bracket2) if len(bracket2) == 1 else bracket2
37
+ return rf'{b1}\s*({g}(?:[^{b1}{b2}"\']|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|{b1}(?:[^{b1}{b2}"\']|"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|(?R))*{b2})*)\s*{b2}'
38
+
39
+ @staticmethod
40
+ def outside_strings(pattern:str = r'.*') -> str:
41
+ """Matches the `pattern` only when it is not found inside a string (`'...'` or `"..."`)."""
42
+ return rf'(?<!["\'])(?:{pattern})(?!["\'])'
43
+
44
+ @staticmethod
45
+ def all_except(disallowed_pattern:str, ignore_pattern:str = '', is_group:bool = False) -> str:
46
+ """Matches everything except `disallowed_pattern`, unless the `disallowed_pattern` is found inside a string (`'...'` or `"..."`).\n
47
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
48
+ 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>
49
+ 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>)."""
50
+ return rf'({"" if is_group else "?:"}(?:(?!{ignore_pattern}).)*(?:(?!{Regex.outside_strings(disallowed_pattern)}).)*)'
51
+
52
+ @staticmethod
53
+ def func_call(func_name:str = None) -> str:
54
+ """Match a function call<br>
55
+ **`1`** function name<br>
56
+ **`2`** the function's arguments\n
57
+ If no `func_name` is given, it will match any function call.\n
58
+ ------------------------------------------------------------------------------------
59
+ **Attention:** Requires non standard library `regex` not standard library `re`!"""
60
+ return r'(?<=\b)(' + (func_name if func_name else r'[\w_]+') + r')\s*' + Regex.brackets("(", ")", is_group=True)
61
+
62
+ @staticmethod
63
+ def rgba_str(fix_sep:str = ',', allow_alpha:bool = False) -> str:
64
+ """Matches an RGBA color inside a string.\n
65
+ --------------------------------------------------------------------------------
66
+ The RGBA color can be in the formats (for `fix_sep = ','`):<br>
67
+ `rgba(r, g, b)`<br>
68
+ `rgba(r, g, b, a)` (if `allow_alpha=True`)<br>
69
+ `(r, g, b)`<br>
70
+ `(r, g, b, a)` (if `allow_alpha=True`)<br>
71
+ `r, g, b`<br>
72
+ `r, g, b, a` (if `allow_alpha=True`)\n
73
+ --------------------------------------------------------------------------------
74
+ ### Valid ranges:<br>
75
+ `r` 0-255 (amount: red)<br>
76
+ `g` 0-255 (amount: green)<br>
77
+ `b` 0-255 (amount: blue)<br>
78
+ `a` 0-1 (float: opacity)\n
79
+ --------------------------------------------------------------------------------
80
+ If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
81
+ can be used to separate the RGB values, including just a space."""
82
+ if fix_sep in (None, ''):
83
+ fix_sep = r'[^0-9A-Z]'
84
+ else:
85
+ fix_sep = _re.escape(fix_sep)
86
+ rgb_part = rf'''((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
87
+ (?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))
88
+ (?:\s*{fix_sep}\s*)((?:0*(?:25[0-5]|2[0-4][0-9]|1?[0-9]{{1,2}})))'''
89
+ return rf'''(?ix)
90
+ (?:rgb|rgba)?\s*(?:\(?\s*{rgb_part}
91
+ (?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
92
+ \s*\)?)''' if allow_alpha else rf'(?ix)(?:rgb|rgba)?\s*(?:\(?\s*{rgb_part}\s*\)?)'
93
+
94
+ @staticmethod
95
+ def hsla_str(fix_sep:str = ',', allow_alpha:bool = False) -> str:
96
+ """Matches a HSLA color inside a string.\n
97
+ --------------------------------------------------------------------------------
98
+ The HSLA color can be in the formats (for `fix_sep = ','`):<br>
99
+ `hsla(h, s, l)`<br>
100
+ `hsla(h, s, l, a)` (if `allow_alpha=True`)<br>
101
+ `(h, s, l)`<br>
102
+ `(h, s, l, a)` (if `allow_alpha=True`)<br>
103
+ `h, s, l`<br>
104
+ `h, s, l, a` (if `allow_alpha=True`)\n
105
+ --------------------------------------------------------------------------------
106
+ ### Valid ranges:<br>
107
+ `h` 0-360 (degrees: hue)<br>
108
+ `s` 0-100 (percentage: saturation)<br>
109
+ `l` 0-100 (percentage: lightness)<br>
110
+ `a` 0-1 (float: opacity)\n
111
+ --------------------------------------------------------------------------------
112
+ If the `fix_sep` is set to nothing, any char that is not a letter or number<br>
113
+ can be used to separate the HSL values, including just a space."""
114
+ if fix_sep in (None, ''):
115
+ fix_sep = r'[^0-9A-Z]'
116
+ else:
117
+ fix_sep = _re.escape(fix_sep)
118
+ hsl_part = rf'''((?:0*(?:360|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])))
119
+ (?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))
120
+ (?:\s*{fix_sep}\s*)((?:0*(?:100|[1-9][0-9]|[0-9])))'''
121
+ return rf'''(?ix)
122
+ (?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}
123
+ (?:(?:\s*{fix_sep}\s*)((?:0*(?:0?\.[0-9]+|1\.0+|[0-9]+\.[0-9]+|[0-9]+))))?
124
+ \s*\)?)''' if allow_alpha else rf'(?ix)(?:hsl|hsla)?\s*(?:\(?\s*{hsl_part}\s*\)?)'
xulbux/xx_string.py ADDED
@@ -0,0 +1,116 @@
1
+ import re as _re
2
+
3
+
4
+
5
+
6
+ class String:
7
+
8
+ @staticmethod
9
+ def to_type(value:str) -> any:
10
+ """Will convert a string to a type."""
11
+ if value.lower() in ['true', 'false']: # BOOLEAN
12
+ return value.lower() == 'true'
13
+ elif value.lower() in ['none', 'null', 'undefined']: # NONE
14
+ return None
15
+ elif value.startswith('[') and value.endswith(']'): # LIST
16
+ return [String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip()]
17
+ elif value.startswith('(') and value.endswith(')'): # TUPLE
18
+ return tuple(String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip())
19
+ elif value.startswith('{') and value.endswith('}'): # SET
20
+ return {String.to_type(item.strip()) for item in value[1:-1].split(',') if item.strip()}
21
+ elif value.startswith('{') and value.endswith('}') and ':' in value: # DICTIONARY
22
+ return {String.to_type(k.strip()): String.to_type(v.strip()) for k, v in [item.split(':') for item in value[1:-1].split(',') if item.strip()]}
23
+ try: # NUMBER (INT OR FLOAT)
24
+ if '.' in value or 'e' in value.lower():
25
+ return float(value)
26
+ else:
27
+ return int(value)
28
+ except ValueError: pass
29
+ if value.startswith(("'", '"')) and value.endswith(("'", '"')): # STRING (WITH OR WITHOUT QUOTES)
30
+ return value[1:-1]
31
+ try: # COMPLEX
32
+ return complex(value)
33
+ except ValueError: pass
34
+ return value # IF NOTHING ELSE MATCHES, RETURN AS IS
35
+
36
+ @staticmethod
37
+ def get_repeated_symbol(string:str, symbol:str) -> int|bool:
38
+ """If the string consists of one repeating `symbol`, it returns the number of times it is repeated.<br>
39
+ If the string doesn't consist of one repeating symbol, it returns `False`."""
40
+ if len(string) == len(symbol) * string.count(symbol):
41
+ return string.count(symbol)
42
+ else:
43
+ return False
44
+
45
+ @staticmethod
46
+ def decompose(case_string:str, seps:str = '-_', lower_all:bool = True) -> list[str]:
47
+ """Will decompose the string (*any type of casing, also mixed*) into parts."""
48
+ return [(part.lower() if lower_all else part) for part in _re.split(rf'(?<=[a-z])(?=[A-Z])|[{seps}]', case_string)]
49
+
50
+ @staticmethod
51
+ def to_camel_case(string:str) -> str:
52
+ """Will convert the string of any type of casing to camel case."""
53
+ return ''.join(part.capitalize() for part in String.decompose(string))
54
+
55
+ @staticmethod
56
+ def to_snake_case(string:str, sep:str = '_', screaming:bool = False) -> str:
57
+ """Will convert the string of any type of casing to snake case."""
58
+ return sep.join(part.upper() if screaming else part for part in String.decompose(string))
59
+
60
+ @staticmethod
61
+ def get_string_lines(string:str, remove_empty_lines:bool = False) -> list[str]:
62
+ """Will split the string into lines."""
63
+ if not remove_empty_lines:
64
+ return string.splitlines()
65
+ lines = string.splitlines()
66
+ if not lines:
67
+ return []
68
+ non_empty_lines = [line for line in lines if line.strip()]
69
+ if not non_empty_lines:
70
+ return []
71
+ return non_empty_lines
72
+
73
+ @staticmethod
74
+ def remove_consecutive_empty_lines(string:str, max_consecutive:int = 0) -> str:
75
+ """Will remove consecutive empty lines from the string.\n
76
+ ----------------------------------------------------------------------------------------------
77
+ If `max_consecutive` is `0`, it will remove all consecutive empty lines.<br>
78
+ If `max_consecutive` is bigger than `0`, it will only allow `max_consecutive` consecutive<br>
79
+ empty lines and everything above it will be cut down to `max_consecutive` empty lines."""
80
+ return _re.sub(r'(\n\s*){2,}', r'\1' * (max_consecutive + 1), string)
81
+
82
+ @staticmethod
83
+ def split_every_chars(string:str, split:int) -> list[str]:
84
+ """Will split the string every `split` characters."""
85
+ return [string[i:i + split] for i in range(0, len(string), split)]
86
+
87
+ @staticmethod
88
+ def multi_strip(string:str, strip_chars:str = ' _-') -> str:
89
+ """Will remove all leading and trailing `strip_chars` from the string."""
90
+ for char in string:
91
+ if char in strip_chars:
92
+ string = string[1:]
93
+ else: break
94
+ for char in string[::-1]:
95
+ if char in strip_chars:
96
+ string = string[:-1]
97
+ else: break
98
+ return string
99
+
100
+ @staticmethod
101
+ def multi_lstrip(string:str, strip_chars:str = ' _-') -> str:
102
+ """Will remove all leading `strip_chars` from the string."""
103
+ for char in string:
104
+ if char in strip_chars:
105
+ string = string[1:]
106
+ else: break
107
+ return string
108
+
109
+ @staticmethod
110
+ def multi_rstrip(string:str, strip_chars:str = ' _-') -> str:
111
+ """Will remove all trailing `strip_chars` from the string."""
112
+ for char in string[::-1]:
113
+ if char in strip_chars:
114
+ string = string[:-1]
115
+ else: break
116
+ return string