xulbux 1.6.1__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_file.py ADDED
@@ -0,0 +1,65 @@
1
+ from .xx_string import String
2
+ from .xx_path import Path
3
+
4
+ import os as _os
5
+
6
+
7
+ class File:
8
+
9
+ @staticmethod
10
+ def rename_extension(
11
+ file: str,
12
+ new_extension: str,
13
+ camel_case_filename: bool = False,
14
+ ) -> str:
15
+ """Rename the extension of a file.\n
16
+ --------------------------------------------------------------------------
17
+ If the `camel_case_filename` parameter is true, the filename will be made
18
+ CamelCase in addition to changing the files extension."""
19
+ directory, filename_with_ext = _os.path.split(file)
20
+ filename = filename_with_ext.split(".")[0]
21
+ if camel_case_filename:
22
+ filename = String.to_camel_case(filename)
23
+ return _os.path.join(directory, f"{filename}{new_extension}")
24
+
25
+ @staticmethod
26
+ def create(
27
+ file: str,
28
+ content: str = "",
29
+ force: bool = False,
30
+ ) -> str:
31
+ """Create a file with ot without content.\n
32
+ ------------------------------------------------------------------------
33
+ The function will throw a `FileExistsError` if the file already exists.
34
+ To always overwrite the file, set the `force` parameter to `True`."""
35
+ if _os.path.exists(file) and not force:
36
+ with open(file, "r", encoding="utf-8") as existing_file:
37
+ existing_content = existing_file.read()
38
+ if existing_content == content:
39
+ raise FileExistsError("Already created this file. (nothing changed)")
40
+ raise FileExistsError("File already exists.")
41
+ with open(file, "w", encoding="utf-8") as f:
42
+ f.write(content)
43
+ full_path = _os.path.abspath(file)
44
+ return full_path
45
+
46
+ @staticmethod
47
+ def extend_or_make_path(
48
+ file: str,
49
+ search_in: str | list[str] = None,
50
+ prefer_base_dir: bool = True,
51
+ correct_paths: bool = False,
52
+ ) -> str:
53
+ """Tries to find the file and extend the path to be absolute and if the file was not found:\n
54
+ Generate the absolute path to the file in the CWD or the running program's base-directory.\n
55
+ ----------------------------------------------------------------------------------------------
56
+ If the `file` is not found in the above directories, it will be searched in the `search_in`
57
+ directory/directories. If the file is still not found, it will return the path to the file in
58
+ the base-dir per default or to the file in the CWD if `prefer_base_dir` is set to `False`.\n
59
+ ----------------------------------------------------------------------------------------------
60
+ If `correct_paths` is true, it is possible to have typos in the `search_in` path/s and it
61
+ will still find the file if it is under one of those paths."""
62
+ try:
63
+ return Path.extend(file, search_in, raise_error=True, correct_path=correct_paths)
64
+ except FileNotFoundError:
65
+ return _os.path.join(Path.get(base_dir=True), file) if prefer_base_dir else _os.path.join(_os.getcwd(), file)
@@ -0,0 +1,305 @@
1
+ """
2
+ Functions to be able to use special (easy) formatting codes directly inside some message (string).\n
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?\n
9
+ Example string with formatting codes:
10
+ ```string
11
+ [bold]This is bold text, [#F08]which is pink now [black|BG:#FF0088] and now it changed`
12
+ to black with a pink background. [_]And this is the boring text, where everything is reset.
13
+ ```
14
+ ⇾ Instead of writing the formats all separate `[…][…][…]` you can join them like this `[…|…|…]`\n
15
+ --------------------------------------------------------------------------------------------------------------------
16
+ You can also automatically reset a certain format, behind text like shown in the following example:
17
+ ```string
18
+ This is normal text [b](which is bold now) but now it was automatically reset to normal.
19
+ ```
20
+ This will only reset formats, that have a reset listed below. Colors and BG-colors won't be reset.\n
21
+ This is what will happen, if you use it with a color-format:
22
+ ```string
23
+ [cyan]This is cyan text [b](which is bold now.) Now it's not bold any more but still cyan.
24
+ ```
25
+ If you want to ignore the `()` brackets you can put a `\\` or `/` between:
26
+ ```string
27
+ [cyan]This is cyan text [b]/(which is bold now.) And now it is still bold and cyan.
28
+ ```
29
+ ⇾ To see these examples in action, you can put them into the `FormatCodes.print()` function.\n
30
+ --------------------------------------------------------------------------------------------------------------------
31
+ All possible formatting codes:
32
+ - HEX colors: `[#F08]` or `[#FF0088]` (with or without leading #)
33
+ - RGB colors: `[rgb(255, 0, 136)]`
34
+ - bright colors: `[bright:#F08]`
35
+ - background colors: `[BG:#F08]`
36
+ - standard cmd colors:
37
+ - `[black]`
38
+ - `[red]`
39
+ - `[green]`
40
+ - `[yellow]`
41
+ - `[blue]`
42
+ - `[magenta]`
43
+ - `[cyan]`
44
+ - `[white]`
45
+ - bright cmd colors: `[bright:black]` or `[br:black]`, `[bright:red]` or `[br:red]`, ...
46
+ - background cmd colors: `[BG:black]`, `[BG:red]`, ...
47
+ - bright background cmd colors: `[BG:bright:black]` or `[BG:br:black]`, `[BG:bright:red]` or `[BG:br:red]`, ...\n
48
+ ⇾ The order of `BG:` and `bright:` or `br:` does not matter.
49
+ - text formats:
50
+ - `[bold]` or `[b]`
51
+ - `[dim]`
52
+ - `[italic]` or `[i]`
53
+ - `[underline]` or `[u]`
54
+ - `[inverse]`, `[invert]` or `[in]`
55
+ - `[hidden]`, `[hide]` or `[h]`
56
+ - `[strikethrough]` or `[s]`
57
+ - `[double-underline]` or `[du]`
58
+ - specific reset: `[_bold]` or `[_b]`, `[_dim]`, ... or `[_color]` or `[_c]`, `[_background]` or `[_bg]`
59
+ - total reset: `[_]` (only if no `default_color` is set, otherwise see ↓ )
60
+ --------------------------------------------------------------------------------------------------------------------
61
+ Special formatting when param `default_color` is set to a color:
62
+ - `[*]` will reset everything, just like `[_]`, but the text-color will remain in `default_color`
63
+ - `[*color]` will reset the text-color, just like `[_color]`, but then also make it `default_color`
64
+ - `[default]` will just color the text in `default_color`,
65
+ - `[BG:default]` will color the background in `default_color`\n
66
+ Unlike the standard cmd colors, the default color can be changed by using the following modifiers:
67
+ - `[l]` will lighten the `default_color` text by `brightness_steps`%
68
+ - `[ll]` will lighten the `default_color` text by `2 × brightness_steps`%
69
+ - `[lll]` will lighten the `default_color` text by `3 × brightness_steps`%
70
+ - ... etc. Same thing for darkening:
71
+ - `[d]` will darken the `default_color` text by `brightness_steps`%
72
+ - `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
73
+ - `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
74
+ - ... etc.\n
75
+ Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.
76
+ """
77
+
78
+ from ._consts_ import ANSI
79
+ from .xx_string import String
80
+ from .xx_regex import Regex
81
+ from .xx_color import *
82
+
83
+ from functools import lru_cache
84
+ import ctypes as _ctypes
85
+ import regex as _rx
86
+ import sys as _sys
87
+ import re as _re
88
+
89
+
90
+ COMPILED = { # PRECOMPILE REGULAR EXPRESSIONS
91
+ "*": _re.compile(r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]"),
92
+ "*color": _re.compile(r"\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]"),
93
+ "format": _rx.compile(
94
+ Regex.brackets("[", "]", is_group=True) + r"(?:\s*([/\\]?)\s*" + Regex.brackets("(", ")", is_group=True) + r")?"
95
+ ),
96
+ "bg?_default": _re.compile(r"(?i)((?:BG\s*:)?)\s*default"),
97
+ "bg_default": _re.compile(r"(?i)BG\s*:\s*default"),
98
+ "modifier": _re.compile(
99
+ rf'(?i)((?:BG\s*:)?)\s*({"|".join([f"{_re.escape(m)}+" for m in ANSI.modifier["lighten"] + ANSI.modifier["darken"]])})$'
100
+ ),
101
+ "rgb": _re.compile(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*$"),
102
+ "hex": _re.compile(r"(?i)^\s*(BG\s*:)?\s*(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})\s*$"),
103
+ }
104
+
105
+
106
+ class FormatCodes:
107
+
108
+ @staticmethod
109
+ def print(
110
+ *values: object,
111
+ default_color: hexa | rgba = None,
112
+ brightness_steps: int = 20,
113
+ sep: str = " ",
114
+ end: str = "\n",
115
+ flush: bool = True,
116
+ ) -> None:
117
+ FormatCodes.__config_console()
118
+ _sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)) + end, default_color, brightness_steps))
119
+ if flush:
120
+ _sys.stdout.flush()
121
+
122
+ @staticmethod
123
+ def input(
124
+ prompt: object = "",
125
+ default_color: hexa | rgba = None,
126
+ brightness_steps: int = 20,
127
+ ) -> str:
128
+ FormatCodes.__config_console()
129
+ return input(FormatCodes.to_ansi(prompt, default_color, brightness_steps))
130
+
131
+ @staticmethod
132
+ def to_ansi(
133
+ string: str, default_color: hexa | rgba = None, brightness_steps: int = 20, _default_start: bool = True
134
+ ) -> str:
135
+ result, bg_kwd, color_pref = string, {"bg"}, {"br", "bright"}
136
+
137
+ if Color.is_valid_rgba(default_color, False):
138
+ use_default = True
139
+ elif Color.is_valid_hexa(default_color, False):
140
+ use_default, default_color = True, Color.to_rgba(default_color)
141
+ else:
142
+ use_default = False
143
+ if use_default:
144
+ string = COMPILED["*"].sub(r"[\1_|default\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
145
+ string = COMPILED["*color"].sub(r"[\1default\2]", string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
146
+
147
+ def is_valid_color(color: str) -> bool:
148
+ return color in ANSI.color_map or Color.is_valid_rgba(color) or Color.is_valid_hexa(color)
149
+
150
+ def replace_keys(match: _re.Match) -> str:
151
+ formats = match.group(1)
152
+ escaped = match.group(2)
153
+ auto_reset_txt = match.group(3)
154
+ if auto_reset_txt and auto_reset_txt.count("[") > 0 and auto_reset_txt.count("]") > 0:
155
+ auto_reset_txt = FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)
156
+ if not formats:
157
+ return match.group(0)
158
+ if formats.count("[") > 0 and formats.count("]") > 0:
159
+ formats = FormatCodes.to_ansi(formats, default_color, brightness_steps, False)
160
+ format_keys = [k.strip() for k in formats.split("|") if k.strip()]
161
+ ansi_formats = [
162
+ r if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)) != k else f"[{k}]"
163
+ for k in format_keys
164
+ ]
165
+ if auto_reset_txt and not escaped:
166
+ reset_keys = []
167
+ for k in format_keys:
168
+ k_lower = k.lower()
169
+ k_parts = k_lower.split(":")
170
+ k_set = set(k_parts)
171
+ if bg_kwd & k_set and len(k_parts) <= 3:
172
+ if k_set & color_pref:
173
+ for i in range(len(k)):
174
+ if is_valid_color(k[i:]):
175
+ reset_keys.extend(["_bg", "_color"])
176
+ break
177
+ else:
178
+ for i in range(len(k)):
179
+ if is_valid_color(k[i:]):
180
+ reset_keys.append("_bg")
181
+ break
182
+ elif is_valid_color(k) or any(
183
+ k_lower.startswith(pref_colon := f"{prefix}:") and is_valid_color(k[len(pref_colon) :])
184
+ for prefix in color_pref
185
+ ):
186
+ reset_keys.append("_color")
187
+ else:
188
+ reset_keys.append(f"_{k}")
189
+ ansi_resets = [
190
+ r
191
+ for k in reset_keys
192
+ if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(
193
+ f"{ANSI.char}{ANSI.start}"
194
+ )
195
+ ]
196
+ else:
197
+ ansi_resets = []
198
+ if not (len(ansi_formats) == 1 and ansi_formats[0].count(f"{ANSI.char}{ANSI.start}") >= 1) and not all(
199
+ f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats
200
+ ):
201
+ return match.group(0)
202
+ return (
203
+ "".join(ansi_formats)
204
+ + (
205
+ f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
206
+ if escaped and auto_reset_txt
207
+ else auto_reset_txt if auto_reset_txt else ""
208
+ )
209
+ + ("" if escaped else "".join(ansi_resets))
210
+ )
211
+
212
+ result = "\n".join(COMPILED["format"].sub(replace_keys, line) for line in string.split("\n"))
213
+ return (FormatCodes.__get_default_ansi(default_color) if _default_start else "") + result if use_default else result
214
+
215
+ @staticmethod
216
+ def escape_ansi(ansi_string: str, escaped_char: str = ANSI.char_esc) -> str:
217
+ """Makes the string printable with the ANSI formats visible."""
218
+ return ansi_string.replace(ANSI.char, escaped_char)
219
+
220
+ @staticmethod
221
+ @lru_cache(maxsize=64)
222
+ def __config_console() -> None:
223
+ _sys.stdout.flush()
224
+ kernel32 = _ctypes.windll.kernel32
225
+ h = kernel32.GetStdHandle(-11)
226
+ mode = _ctypes.c_ulong()
227
+ kernel32.GetConsoleMode(h, _ctypes.byref(mode))
228
+ kernel32.SetConsoleMode(h, mode.value | 0x0004)
229
+
230
+ @staticmethod
231
+ def __get_default_ansi(
232
+ default_color: tuple,
233
+ format_key: str = None,
234
+ brightness_steps: int = None,
235
+ _modifiers: tuple[str, str] = (ANSI.modifier["lighten"], ANSI.modifier["darken"]),
236
+ ) -> str | None:
237
+ if not brightness_steps or (format_key and COMPILED["bg?_default"].search(format_key)):
238
+ return (ANSI.seq_bg_color if format_key and COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
239
+ *default_color[:3]
240
+ )
241
+ if not (format_key in _modifiers[0] or format_key in _modifiers[1]):
242
+ return None
243
+ match = COMPILED["modifier"].match(format_key)
244
+ if not match:
245
+ return None
246
+ is_bg, modifiers = match.groups()
247
+ adjust = 0
248
+ for mod in _modifiers[0] + _modifiers[1]:
249
+ adjust = String.single_char_repeats(modifiers, mod)
250
+ if adjust and adjust > 0:
251
+ modifiers = mod
252
+ break
253
+ if adjust == 0:
254
+ return None
255
+ elif modifiers in _modifiers[0]:
256
+ new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust)
257
+ elif modifiers in _modifiers[1]:
258
+ new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust)
259
+ return (ANSI.seq_bg_color if is_bg else ANSI.seq_color).format(*new_rgb[:3])
260
+
261
+ @staticmethod
262
+ def __get_replacement(format_key: str, default_color: rgba = None, brightness_steps: int = 20) -> str:
263
+ """Gives you the corresponding ANSI code for the given format key.
264
+ If `default_color` is not `None`, the text color will be `default_color` if all formats
265
+ are reset or you can get lighter or darker version of `default_color` (also as BG)"""
266
+ use_default = default_color and Color.is_valid_rgba(default_color, False)
267
+ _format_key, format_key = format_key, ( # NORMALIZE THE FORMAT KEY (+ SAVE ORIGINAL)
268
+ "bg:" if "bg" in (parts := format_key.replace(" ", "").lower().split(":")) else ""
269
+ ) + ("bright:" if any(x in parts for x in ("bright", "br")) else "") + ":".join(
270
+ p for p in parts if p not in ("bg", "bright", "br")
271
+ )
272
+ if use_default:
273
+ if new_default_color := FormatCodes.__get_default_ansi(default_color, format_key, brightness_steps):
274
+ return new_default_color
275
+ for map_key in ANSI.codes_map:
276
+ if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
277
+ return ANSI.seq().format(
278
+ next(
279
+ (
280
+ v
281
+ for k, v in ANSI.codes_map.items()
282
+ if format_key == k or (isinstance(k, tuple) and format_key in k)
283
+ ),
284
+ None,
285
+ )
286
+ )
287
+ rgb_match = _re.match(COMPILED["rgb"], format_key)
288
+ hex_match = _re.match(COMPILED["hex"], format_key)
289
+ try:
290
+ if rgb_match:
291
+ is_bg = rgb_match.group(1)
292
+ r, g, b = map(int, rgb_match.groups()[1:])
293
+ if Color.is_valid_rgba((r, g, b)):
294
+ return ANSI.seq_bg_color.format(r, g, b) if is_bg else ANSI.seq_color.format(r, g, b)
295
+ elif hex_match:
296
+ is_bg = hex_match.group(1)
297
+ rgb = Color.to_rgba(hex_match.group(2))
298
+ return (
299
+ ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2])
300
+ if is_bg
301
+ else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
302
+ )
303
+ except Exception:
304
+ pass
305
+ return _format_key
xulbux/xx_json.py ADDED
@@ -0,0 +1,106 @@
1
+ from .xx_data import Data
2
+ from .xx_file import File
3
+
4
+ import json as _json
5
+ import os as _os
6
+
7
+
8
+ class Json:
9
+
10
+ @staticmethod
11
+ def read(
12
+ json_file: str,
13
+ comment_start: str = ">>",
14
+ comment_end: str = "<<",
15
+ return_original: bool = False,
16
+ ) -> dict | tuple[dict, dict]:
17
+ """Read JSON files, ignoring comments.\n
18
+ -------------------------------------------------------------------------
19
+ If only `comment_start` is found at the beginning of an item,
20
+ the whole item is counted as a comment and therefore ignored.
21
+ If `comment_start` and `comment_end` are found inside an item,
22
+ the the section from `comment_start` to `comment_end` is ignored.
23
+ If `return_original` is set to `True`, the original JSON is returned
24
+ additionally. (returns: `[processed_json, original_json]`)"""
25
+ if not json_file.endswith(".json"):
26
+ json_file += ".json"
27
+ file_path = File.extend_or_make_path(json_file, prefer_base_dir=True)
28
+ with open(file_path, "r") as f:
29
+ content = f.read()
30
+ try:
31
+ data = _json.loads(content)
32
+ except _json.JSONDecodeError as e:
33
+ raise ValueError(f"Error parsing JSON in '{file_path}': {str(e)}")
34
+ processed_data = Data.remove_comments(data, comment_start, comment_end)
35
+ if not processed_data:
36
+ raise ValueError(f"The JSON file '{file_path}' is empty or contains only comments.")
37
+ return (processed_data, data) if return_original else processed_data
38
+
39
+ @staticmethod
40
+ def create(
41
+ content: dict,
42
+ new_file: str = "config",
43
+ indent: int = 2,
44
+ compactness: int = 1,
45
+ force: bool = False,
46
+ ) -> str:
47
+ if not new_file.endswith(".json"):
48
+ new_file += ".json"
49
+ file_path = File.extend_or_make_path(new_file, prefer_base_dir=True)
50
+ if _os.path.exists(file_path) and not force:
51
+ with open(file_path, "r", encoding="utf-8") as existing_f:
52
+ existing_content = _json.load(existing_f)
53
+ if existing_content == content:
54
+ raise FileExistsError("Already created this file. (nothing changed)")
55
+ raise FileExistsError("File already exists.")
56
+ with open(file_path, "w", encoding="utf-8") as f:
57
+ f.write(Data.to_str(content, indent, compactness, as_json=True))
58
+ full_path = _os.path.abspath(file_path)
59
+ return full_path
60
+
61
+ @staticmethod
62
+ def update(
63
+ json_file: str,
64
+ update_values: str | list[str],
65
+ comment_start: str = ">>",
66
+ comment_end: str = "<<",
67
+ sep: tuple[str, str] = ("->", "::"),
68
+ ) -> None:
69
+ """Function to easily update single/multiple values inside JSON files.\n
70
+ ------------------------------------------------------------------------------------------------------
71
+ The param `json_file` is the path to the JSON file or just the name of the JSON file to be updated.\n
72
+ ------------------------------------------------------------------------------------------------------
73
+ The param `update_values` is a sort of path (or a list of paths) to the value/s to be updated, with
74
+ the new value at the end of the path.\n
75
+ In this example:
76
+ ```python
77
+ {
78
+ 'healthy': {
79
+ 'fruit': ['apples', 'bananas', 'oranges'],
80
+ 'vegetables': ['carrots', 'broccoli', 'celery']
81
+ }
82
+ }
83
+ ```
84
+ ... if you want to change the value of `'apples'` to `'strawberries'`, `update_values` would be
85
+ `healthy->fruit->apples::strawberries` or if you don't know that the value to update is `apples` you
86
+ can also use the position of the value, so `healthy->fruit->0::strawberries`.\n
87
+ ⇾ If the path from `update_values` doesn't exist, it will be created.\n
88
+ ------------------------------------------------------------------------------------------------------
89
+ If only `comment_start` is found at the beginning of an item, the whole item is counted as a comment
90
+ and therefore ignored. If `comment_start` and `comment_end` are found inside an item, the the section
91
+ from `comment_start` to `comment_end` is ignored."""
92
+ if isinstance(update_values, str):
93
+ update_values = [update_values]
94
+ valid_entries = [
95
+ (parts[0].strip(), parts[1])
96
+ for update_value in update_values
97
+ if len(parts := update_value.split(str(sep[1]).strip())) == 2
98
+ ]
99
+ value_paths, new_values = zip(*valid_entries) if valid_entries else ([], [])
100
+ processed_data, data = Json.read(json_file, comment_start, comment_end, return_original=True)
101
+ update = []
102
+ for value_path, new_value in zip(value_paths, new_values):
103
+ path_id = Data.get_path_id(processed_data, value_path)
104
+ update.append(f"{path_id}::{new_value}")
105
+ updated = Data.set_value_by_path_id(data, update)
106
+ Json.create(updated, json_file, force=True)
xulbux/xx_path.py ADDED
@@ -0,0 +1,107 @@
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
+ class Path:
9
+
10
+ @staticmethod
11
+ def get(cwd: bool = False, base_dir: bool = False) -> str | list[str]:
12
+ paths = []
13
+ if cwd:
14
+ paths.append(_os.getcwd())
15
+ if base_dir:
16
+ if getattr(_sys, "frozen", False):
17
+ base_path = _os.path.dirname(_sys.executable)
18
+ else:
19
+ main_module = _sys.modules["__main__"]
20
+ if hasattr(main_module, "__file__"):
21
+ base_path = _os.path.dirname(_os.path.abspath(main_module.__file__))
22
+ elif (
23
+ hasattr(main_module, "__spec__") and main_module.__spec__ and getattr(main_module.__spec__, "origin", None)
24
+ ):
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
+
36
+ def get_closest_match(dir: str, part: str) -> str | None:
37
+ try:
38
+ files_and_dirs = _os.listdir(dir)
39
+ matches = _difflib.get_close_matches(part, files_and_dirs, n=1, cutoff=0.6)
40
+ return matches[0] if matches else None
41
+ except Exception:
42
+ return None
43
+
44
+ def find_path(start: str, parts: list[str]) -> str | None:
45
+ current = start
46
+ for part in parts:
47
+ if _os.path.isfile(current):
48
+ return current
49
+ closest_match = get_closest_match(current, part) if correct_path else part
50
+ current = _os.path.join(current, closest_match) if closest_match else None
51
+ if current is None:
52
+ return None
53
+ return current if _os.path.exists(current) and current != start else None
54
+
55
+ def expand_env_path(p: str) -> str:
56
+ if "%" not in p:
57
+ return p
58
+ parts = p.split("%")
59
+ for i in range(1, len(parts), 2):
60
+ if parts[i].upper() in _os.environ:
61
+ parts[i] = _os.environ[parts[i].upper()]
62
+ return "".join(parts)
63
+
64
+ path = _os.path.normpath(expand_env_path(path))
65
+ if _os.path.isabs(path):
66
+ drive, rel_path = _os.path.splitdrive(path)
67
+ rel_path = rel_path.lstrip(_os.sep)
68
+ search_dirs = (drive + _os.sep) if drive else [_os.sep]
69
+ else:
70
+ rel_path = path.lstrip(_os.sep)
71
+ base_dir = Path.get(base_dir=True)
72
+ search_dirs = (
73
+ _os.getcwd(),
74
+ base_dir,
75
+ _os.path.expanduser("~"),
76
+ _tempfile.gettempdir(),
77
+ )
78
+ if search_in:
79
+ search_dirs.extend([search_in] if isinstance(search_in, str) else search_in)
80
+ path_parts = rel_path.split(_os.sep)
81
+ for search_dir in search_dirs:
82
+ full_path = _os.path.join(search_dir, rel_path)
83
+ if _os.path.exists(full_path):
84
+ return full_path
85
+ match = find_path(search_dir, path_parts) if correct_path else None
86
+ if match:
87
+ return match
88
+ if raise_error:
89
+ raise FileNotFoundError(f"Path '{path}' not found in specified directories.")
90
+ return _os.path.join(search_dirs[0], rel_path)
91
+
92
+ @staticmethod
93
+ def remove(path: str, only_content: bool = False) -> None:
94
+ if not _os.path.exists(path):
95
+ return None
96
+ if not only_content:
97
+ _shutil.rmtree(path)
98
+ elif _os.path.isdir(path):
99
+ for filename in _os.listdir(path):
100
+ file_path = _os.path.join(path, filename)
101
+ try:
102
+ if _os.path.isfile(file_path) or _os.path.islink(file_path):
103
+ _os.unlink(file_path)
104
+ elif _os.path.isdir(file_path):
105
+ _shutil.rmtree(file_path)
106
+ except Exception as e:
107
+ raise Exception(f"Failed to delete {file_path}. Reason: {e}")