xulbux 1.5.5__py3-none-any.whl → 1.5.7__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 +20 -10
- xulbux/{__help__.py → _cli_.py} +15 -37
- xulbux/_consts_.py +108 -111
- xulbux/xx_cmd.py +245 -104
- xulbux/xx_code.py +28 -25
- xulbux/xx_color.py +330 -182
- xulbux/xx_data.py +214 -90
- xulbux/xx_env_vars.py +36 -23
- xulbux/xx_file.py +20 -14
- xulbux/xx_format_codes.py +154 -88
- xulbux/xx_json.py +36 -16
- xulbux/xx_path.py +38 -23
- xulbux/xx_regex.py +44 -27
- xulbux/xx_string.py +75 -47
- xulbux/xx_system.py +37 -26
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/METADATA +14 -10
- xulbux-1.5.7.dist-info/RECORD +20 -0
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/WHEEL +1 -1
- xulbux-1.5.7.dist-info/entry_points.txt +3 -0
- xulbux-1.5.5.dist-info/RECORD +0 -20
- xulbux-1.5.5.dist-info/entry_points.txt +0 -2
- {xulbux-1.5.5.dist-info → xulbux-1.5.7.dist-info}/licenses/LICENSE +0 -0
xulbux/xx_file.py
CHANGED
|
@@ -4,47 +4,53 @@ from .xx_path import *
|
|
|
4
4
|
import os as _os
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
7
|
class File:
|
|
10
8
|
|
|
11
9
|
@staticmethod
|
|
12
|
-
def _make_path(
|
|
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:
|
|
13
17
|
"""Get the path to a file in the cwd, the base-dir, or predefined directories.\n
|
|
14
18
|
--------------------------------------------------------------------------------------
|
|
15
19
|
If the `filename` is not found in the above directories, it will be searched<br>
|
|
16
20
|
in the `search_in` directory/directories. If the file is still not found, it will<br>
|
|
17
21
|
return the path to the file in the base-dir per default or to the file in the<br>
|
|
18
22
|
cwd if `prefer_base_dir` is set to `False`."""
|
|
19
|
-
if not filename.lower().endswith(f
|
|
20
|
-
filename = f
|
|
23
|
+
if not filename.lower().endswith(f".{filetype.lower()}"):
|
|
24
|
+
filename = f"{filename}.{filetype.lower()}"
|
|
21
25
|
try:
|
|
22
26
|
return Path.extend(filename, search_in, True, correct_path)
|
|
23
27
|
except FileNotFoundError:
|
|
24
|
-
return
|
|
28
|
+
return (
|
|
29
|
+
_os.path.join(Path.get(base_dir=True), filename) if prefer_base_dir else _os.path.join(_os.getcwd(), filename)
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
@staticmethod
|
|
27
|
-
def rename_extension(file_path:str, new_extension:str) -> str:
|
|
33
|
+
def rename_extension(file_path: str, new_extension: str) -> str:
|
|
28
34
|
directory, filename_with_ext = _os.path.split(file_path)
|
|
29
|
-
filename = filename_with_ext.split(
|
|
35
|
+
filename = filename_with_ext.split(".")[0]
|
|
30
36
|
camel_case_filename = String.to_camel_case(filename)
|
|
31
|
-
new_filename = f
|
|
37
|
+
new_filename = f"{camel_case_filename}{new_extension}"
|
|
32
38
|
new_file_path = _os.path.join(directory, new_filename)
|
|
33
39
|
return new_file_path
|
|
34
40
|
|
|
35
41
|
@staticmethod
|
|
36
|
-
def create(content:str =
|
|
42
|
+
def create(content: str = "", file: str = "new_file.txt", force: bool = False) -> str:
|
|
37
43
|
"""Create a file with ot without content.\n
|
|
38
44
|
----------------------------------------------------------------------------
|
|
39
45
|
The function will throw a `FileExistsError` if the file already exists.<br>
|
|
40
46
|
To overwrite the file, set the `force` parameter to `True`."""
|
|
41
47
|
if _os.path.exists(file) and not force:
|
|
42
|
-
with open(file,
|
|
48
|
+
with open(file, "r", encoding="utf-8") as existing_file:
|
|
43
49
|
existing_content = existing_file.read()
|
|
44
50
|
if existing_content == content:
|
|
45
|
-
raise FileExistsError(
|
|
46
|
-
raise FileExistsError(
|
|
47
|
-
with open(file,
|
|
51
|
+
raise FileExistsError("Already created this file. (nothing changed)")
|
|
52
|
+
raise FileExistsError("File already exists.")
|
|
53
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
48
54
|
f.write(content)
|
|
49
55
|
full_path = _os.path.abspath(file)
|
|
50
56
|
return full_path
|
xulbux/xx_format_codes.py
CHANGED
|
@@ -64,127 +64,201 @@ Unlike the standard cmd colors, the default color can be changed by using the fo
|
|
|
64
64
|
- `[dd]` will darken the `default_color` text by `2 × brightness_steps`%
|
|
65
65
|
- `[ddd]` will darken the `default_color` text by `3 × brightness_steps`%
|
|
66
66
|
- ... etc.\n
|
|
67
|
-
Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions
|
|
68
|
-
This can also be changed by changing the param `_modifiers = ('+l', '-d')`.
|
|
67
|
+
Per default, you can also use `+` and `-` to get lighter and darker `default_color` versions.
|
|
69
68
|
"""
|
|
70
69
|
|
|
71
|
-
|
|
72
70
|
from ._consts_ import ANSI
|
|
73
71
|
from .xx_string import *
|
|
74
72
|
from .xx_regex import *
|
|
75
73
|
from .xx_color import *
|
|
76
74
|
from .xx_data import *
|
|
77
75
|
|
|
76
|
+
from functools import lru_cache
|
|
78
77
|
import ctypes as _ctypes
|
|
79
78
|
import regex as _rx
|
|
80
79
|
import sys as _sys
|
|
81
80
|
import re as _re
|
|
82
81
|
|
|
83
82
|
|
|
83
|
+
COMPILED = { # PRECOMPILE REGULAR EXPRESSIONS
|
|
84
|
+
"*": _re.compile(r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]"),
|
|
85
|
+
"*color": _re.compile(r"\[\s*([^]_]*?)\s*\*color\s*([^]_]*?)\]"),
|
|
86
|
+
"format": _rx.compile(
|
|
87
|
+
Regex.brackets("[", "]", is_group=True) + r"(?:\s*([/\\]?)\s*" + Regex.brackets("(", ")", is_group=True) + r")?"
|
|
88
|
+
),
|
|
89
|
+
"bg?_default": _re.compile(r"(?i)((?:BG\s*:)?)\s*default"),
|
|
90
|
+
"bg_default": _re.compile(r"(?i)BG\s*:\s*default"),
|
|
91
|
+
"modifier": _re.compile(
|
|
92
|
+
rf'(?i)((?:BG\s*:)?)\s*({"|".join([f"{_re.escape(m)}+" for m in ANSI.modifier["lighten"] + ANSI.modifier["darken"]])})$'
|
|
93
|
+
),
|
|
94
|
+
"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*$"),
|
|
95
|
+
"hex": _re.compile(r"(?i)^\s*(BG\s*:)?\s*(?:#|0x)?([0-9A-F]{6}|[0-9A-F]{3})\s*$"),
|
|
96
|
+
}
|
|
84
97
|
|
|
85
98
|
|
|
86
99
|
class FormatCodes:
|
|
87
100
|
|
|
88
101
|
@staticmethod
|
|
89
|
-
def print(
|
|
102
|
+
def print(
|
|
103
|
+
*values: object,
|
|
104
|
+
default_color: hexa | rgba = None,
|
|
105
|
+
brightness_steps: int = 20,
|
|
106
|
+
sep: str = " ",
|
|
107
|
+
end: str = "\n",
|
|
108
|
+
flush: bool = True,
|
|
109
|
+
) -> None:
|
|
90
110
|
FormatCodes.__config_console()
|
|
91
|
-
_sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)), default_color, brightness_steps)
|
|
92
|
-
|
|
111
|
+
_sys.stdout.write(FormatCodes.to_ansi(sep.join(map(str, values)) + end, default_color, brightness_steps))
|
|
112
|
+
if flush:
|
|
113
|
+
_sys.stdout.flush()
|
|
93
114
|
|
|
94
115
|
@staticmethod
|
|
95
|
-
def input(
|
|
116
|
+
def input(
|
|
117
|
+
prompt: object = "",
|
|
118
|
+
default_color: hexa | rgba = None,
|
|
119
|
+
brightness_steps: int = 20,
|
|
120
|
+
) -> str:
|
|
96
121
|
FormatCodes.__config_console()
|
|
97
122
|
return input(FormatCodes.to_ansi(prompt, default_color, brightness_steps))
|
|
98
123
|
|
|
99
124
|
@staticmethod
|
|
100
|
-
def to_ansi(
|
|
101
|
-
|
|
125
|
+
def to_ansi(
|
|
126
|
+
string: str, default_color: hexa | rgba = None, brightness_steps: int = 20, _default_start: bool = True
|
|
127
|
+
) -> str:
|
|
128
|
+
result = ""
|
|
129
|
+
if Color.is_valid_rgba(default_color, False):
|
|
130
|
+
use_default = True
|
|
131
|
+
elif Color.is_valid_hexa(default_color, False):
|
|
132
|
+
use_default, default_color = True, Color.to_rgba(default_color)
|
|
133
|
+
else:
|
|
134
|
+
use_default = False
|
|
102
135
|
if use_default:
|
|
103
|
-
string =
|
|
104
|
-
string =
|
|
105
|
-
|
|
106
|
-
|
|
136
|
+
string = COMPILED["*"].sub(r"[\1_|default\2]", string) # REPLACE `[…|*|…]` WITH `[…|_|default|…]`
|
|
137
|
+
string = COMPILED["*color"].sub(r"[\1default\2]", string) # REPLACE `[…|*color|…]` WITH `[…|default|…]`
|
|
138
|
+
|
|
139
|
+
def replace_keys(match: _re.Match) -> str:
|
|
140
|
+
format_keys = match.group(1)
|
|
141
|
+
esc = match.group(2)
|
|
142
|
+
auto_reset_txt = match.group(3)
|
|
107
143
|
if not format_keys:
|
|
108
144
|
return match.group(0)
|
|
145
|
+
format_keys = [k.strip() for k in format_keys.split("|") if k.strip()]
|
|
146
|
+
ansi_formats = [FormatCodes.__get_replacement(k, default_color, brightness_steps) for k in format_keys]
|
|
147
|
+
if auto_reset_txt and not esc:
|
|
148
|
+
reset_keys = [
|
|
149
|
+
(
|
|
150
|
+
"_color"
|
|
151
|
+
if k in ANSI.color_map or Color.is_valid_rgba(k) or Color.is_valid_hexa(k)
|
|
152
|
+
else (
|
|
153
|
+
"_bg"
|
|
154
|
+
if ({"bg", "bright", "br"} & set(k.lower().split(":")))
|
|
155
|
+
and len(k.split(":")) <= 3
|
|
156
|
+
and any(
|
|
157
|
+
k[i:] in ANSI.color_map or Color.is_valid_rgba(k[i:]) or Color.is_valid_hexa(k[i:])
|
|
158
|
+
for i in range(len(k))
|
|
159
|
+
)
|
|
160
|
+
else f"_{k}"
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
for k in format_keys
|
|
164
|
+
]
|
|
165
|
+
ansi_resets = [
|
|
166
|
+
r
|
|
167
|
+
for k in reset_keys
|
|
168
|
+
if (r := FormatCodes.__get_replacement(k, default_color, brightness_steps)).startswith(
|
|
169
|
+
f"{ANSI.char}{ANSI.start}"
|
|
170
|
+
)
|
|
171
|
+
]
|
|
109
172
|
else:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
173
|
+
ansi_resets = []
|
|
174
|
+
if not all(f.startswith(f"{ANSI.char}{ANSI.start}") for f in ansi_formats):
|
|
175
|
+
return match.group(0)
|
|
176
|
+
return (
|
|
177
|
+
"".join(ansi_formats)
|
|
178
|
+
+ (
|
|
179
|
+
f"({FormatCodes.to_ansi(auto_reset_txt, default_color, brightness_steps, False)})"
|
|
180
|
+
if esc and auto_reset_txt
|
|
181
|
+
else auto_reset_txt if auto_reset_txt else ""
|
|
182
|
+
)
|
|
183
|
+
+ ("" if esc else "".join(ansi_resets))
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
(FormatCodes.__get_default_ansi(default_color) if _default_start else "")
|
|
188
|
+
+ (result := "\n".join(COMPILED["format"].sub(replace_keys, line) for line in string.split("\n")))
|
|
189
|
+
if use_default
|
|
190
|
+
else result
|
|
191
|
+
)
|
|
121
192
|
|
|
122
193
|
@staticmethod
|
|
194
|
+
@lru_cache(maxsize=64)
|
|
123
195
|
def __config_console() -> None:
|
|
124
196
|
_sys.stdout.flush()
|
|
125
197
|
kernel32 = _ctypes.windll.kernel32
|
|
126
198
|
h = kernel32.GetStdHandle(-11)
|
|
127
199
|
mode = _ctypes.c_ulong()
|
|
128
200
|
kernel32.GetConsoleMode(h, _ctypes.byref(mode))
|
|
129
|
-
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
201
|
+
kernel32.SetConsoleMode(h, mode.value | 0x0004)
|
|
130
202
|
|
|
131
203
|
@staticmethod
|
|
132
|
-
def __get_default_ansi(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
204
|
+
def __get_default_ansi(
|
|
205
|
+
default_color: tuple,
|
|
206
|
+
format_key: str = None,
|
|
207
|
+
brightness_steps: int = None,
|
|
208
|
+
_modifiers: tuple[str, str] = (ANSI.modifier["lighten"], ANSI.modifier["darken"]),
|
|
209
|
+
) -> str | None:
|
|
210
|
+
if not brightness_steps or (format_key and COMPILED["bg?_default"].search(format_key)):
|
|
211
|
+
return (ANSI.seq_bg_color if format_key and COMPILED["bg_default"].search(format_key) else ANSI.seq_color).format(
|
|
212
|
+
*default_color[:3]
|
|
213
|
+
)
|
|
214
|
+
if not (format_key in _modifiers[0] or format_key in _modifiers[1]):
|
|
141
215
|
return None
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
216
|
+
match = COMPILED["modifier"].match(format_key)
|
|
217
|
+
if not match:
|
|
218
|
+
return None
|
|
219
|
+
is_bg, modifiers = match.groups()
|
|
220
|
+
adjust = 0
|
|
221
|
+
for mod in _modifiers[0] + _modifiers[1]:
|
|
222
|
+
adjust = String.single_char_repeats(modifiers, mod)
|
|
223
|
+
if adjust and adjust > 0:
|
|
224
|
+
modifiers = mod
|
|
148
225
|
break
|
|
149
|
-
if
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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])
|
|
226
|
+
if adjust == 0:
|
|
227
|
+
return None
|
|
228
|
+
elif modifiers in _modifiers[0]:
|
|
229
|
+
new_rgb = Color.adjust_lightness(default_color, (brightness_steps / 100) * adjust)
|
|
230
|
+
elif modifiers in _modifiers[1]:
|
|
231
|
+
new_rgb = Color.adjust_lightness(default_color, -(brightness_steps / 100) * adjust)
|
|
232
|
+
return (ANSI.seq_bg_color if is_bg else ANSI.seq_color).format(*new_rgb[:3])
|
|
157
233
|
|
|
158
234
|
@staticmethod
|
|
159
|
-
def __get_replacement(format_key:str, default_color:
|
|
235
|
+
def __get_replacement(format_key: str, default_color: rgba = None, brightness_steps: int = 20) -> str:
|
|
160
236
|
"""Gives you the corresponding ANSI code for the given format key.<br>
|
|
161
237
|
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)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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)
|
|
238
|
+
are reset or you can get lighter or darker version of `default_color` (also as BG)"""
|
|
239
|
+
use_default = default_color and Color.is_valid_rgba(default_color, False)
|
|
240
|
+
_format_key, format_key = format_key, ( # NORMALIZE THE FORMAT KEY (+ SAVE ORIGINAL)
|
|
241
|
+
"bg:" if "bg" in (parts := format_key.replace(" ", "").lower().split(":")) else ""
|
|
242
|
+
) + ("bright:" if any(x in parts for x in ("bright", "br")) else "") + ":".join(
|
|
243
|
+
p for p in parts if p not in ("bg", "bright", "br")
|
|
244
|
+
)
|
|
180
245
|
if use_default:
|
|
181
|
-
new_default_color
|
|
182
|
-
if new_default_color:
|
|
246
|
+
if new_default_color := FormatCodes.__get_default_ansi(default_color, format_key, brightness_steps):
|
|
183
247
|
return new_default_color
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
248
|
+
for map_key in ANSI.codes_map:
|
|
249
|
+
if (isinstance(map_key, tuple) and format_key in map_key) or format_key == map_key:
|
|
250
|
+
return ANSI.seq().format(
|
|
251
|
+
next(
|
|
252
|
+
(
|
|
253
|
+
v
|
|
254
|
+
for k, v in ANSI.codes_map.items()
|
|
255
|
+
if format_key == k or (isinstance(k, tuple) and format_key in k)
|
|
256
|
+
),
|
|
257
|
+
None,
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
rgb_match = _re.match(COMPILED["rgb"], format_key)
|
|
261
|
+
hex_match = _re.match(COMPILED["hex"], format_key)
|
|
188
262
|
try:
|
|
189
263
|
if rgb_match:
|
|
190
264
|
is_bg = rgb_match.group(1)
|
|
@@ -194,19 +268,11 @@ class FormatCodes:
|
|
|
194
268
|
elif hex_match:
|
|
195
269
|
is_bg = hex_match.group(1)
|
|
196
270
|
rgb = Color.to_rgba(hex_match.group(2))
|
|
197
|
-
return
|
|
198
|
-
|
|
271
|
+
return (
|
|
272
|
+
ANSI.seq_bg_color.format(rgb[0], rgb[1], rgb[2])
|
|
273
|
+
if is_bg
|
|
274
|
+
else ANSI.seq_color.format(rgb[0], rgb[1], rgb[2])
|
|
275
|
+
)
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
199
278
|
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
CHANGED
|
@@ -5,12 +5,15 @@ import json as _json
|
|
|
5
5
|
import os as _os
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
8
|
class Json:
|
|
11
9
|
|
|
12
10
|
@staticmethod
|
|
13
|
-
def read(
|
|
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]:
|
|
14
17
|
"""Read JSON files, ignoring comments.\n
|
|
15
18
|
-------------------------------------------------------------------------
|
|
16
19
|
If only `comment_start` is found at the beginning of an item,<br>
|
|
@@ -19,8 +22,8 @@ class Json:
|
|
|
19
22
|
the the section from `comment_start` to `comment_end` is ignored.<br>
|
|
20
23
|
If `return_original` is set to `True`, the original JSON is returned<br>
|
|
21
24
|
additionally. (returns: `[processed_json, original_json]`)"""
|
|
22
|
-
file_path = File._make_path(json_file,
|
|
23
|
-
with open(file_path,
|
|
25
|
+
file_path = File._make_path(json_file, "json", prefer_base_dir=True)
|
|
26
|
+
with open(file_path, "r") as f:
|
|
24
27
|
content = f.read()
|
|
25
28
|
try:
|
|
26
29
|
data = _json.loads(content)
|
|
@@ -32,21 +35,33 @@ class Json:
|
|
|
32
35
|
return (processed_data, data) if return_original else processed_data
|
|
33
36
|
|
|
34
37
|
@staticmethod
|
|
35
|
-
def create(
|
|
36
|
-
|
|
38
|
+
def create(
|
|
39
|
+
content: dict,
|
|
40
|
+
new_file: str = "config",
|
|
41
|
+
indent: int = 2,
|
|
42
|
+
compactness: int = 1,
|
|
43
|
+
force: bool = False,
|
|
44
|
+
) -> str:
|
|
45
|
+
file_path = File._make_path(new_file, "json", prefer_base_dir=True)
|
|
37
46
|
if _os.path.exists(file_path) and not force:
|
|
38
|
-
with open(file_path,
|
|
47
|
+
with open(file_path, "r", encoding="utf-8") as existing_f:
|
|
39
48
|
existing_content = _json.load(existing_f)
|
|
40
49
|
if existing_content == content:
|
|
41
|
-
raise FileExistsError(
|
|
42
|
-
raise FileExistsError(
|
|
43
|
-
with open(file_path,
|
|
50
|
+
raise FileExistsError("Already created this file. (nothing changed)")
|
|
51
|
+
raise FileExistsError("File already exists.")
|
|
52
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
44
53
|
f.write(Data.to_str(content, indent, compactness, as_json=True))
|
|
45
54
|
full_path = _os.path.abspath(file_path)
|
|
46
55
|
return full_path
|
|
47
56
|
|
|
48
57
|
@staticmethod
|
|
49
|
-
def update(
|
|
58
|
+
def update(
|
|
59
|
+
json_file: str,
|
|
60
|
+
update_values: str | list[str],
|
|
61
|
+
comment_start: str = ">>",
|
|
62
|
+
comment_end: str = "<<",
|
|
63
|
+
sep: tuple[str, str] = ("->", "::"),
|
|
64
|
+
) -> None:
|
|
50
65
|
"""Function to easily update single/multiple values inside JSON files.\n
|
|
51
66
|
--------------------------------------------------------------------------------------------------------
|
|
52
67
|
The param `json_file` is the path to the JSON file or just the name of the JSON file to be updated.\n
|
|
@@ -67,15 +82,20 @@ class Json:
|
|
|
67
82
|
--------------------------------------------------------------------------------------------------------
|
|
68
83
|
If only `comment_start` is found at the beginning of an item, the whole item is counted<br>
|
|
69
84
|
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.
|
|
85
|
+
inside an item, the the section from `comment_start` to `comment_end` is ignored.
|
|
86
|
+
"""
|
|
71
87
|
if isinstance(update_values, str):
|
|
72
88
|
update_values = [update_values]
|
|
73
|
-
valid_entries = [
|
|
74
|
-
|
|
89
|
+
valid_entries = [
|
|
90
|
+
(parts[0].strip(), parts[1])
|
|
91
|
+
for update_value in update_values
|
|
92
|
+
if len(parts := update_value.split(str(sep[1]).strip())) == 2
|
|
93
|
+
]
|
|
94
|
+
value_paths, new_values = zip(*valid_entries) if valid_entries else ([], [])
|
|
75
95
|
processed_data, data = Json.read(json_file, comment_start, comment_end, return_original=True)
|
|
76
96
|
update = []
|
|
77
97
|
for value_path, new_value in zip(value_paths, new_values):
|
|
78
98
|
path_id = Data.get_path_id(processed_data, value_path)
|
|
79
|
-
update.append(f
|
|
99
|
+
update.append(f"{path_id}::{new_value}")
|
|
80
100
|
updated = Data.set_value_by_path_id(data, update)
|
|
81
101
|
Json.create(updated, json_file, force=True)
|
xulbux/xx_path.py
CHANGED
|
@@ -5,41 +5,48 @@ import sys as _sys
|
|
|
5
5
|
import os as _os
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
8
|
class Path:
|
|
11
9
|
|
|
12
10
|
@staticmethod
|
|
13
|
-
def get(cwd:bool = False, base_dir:bool = False) -> str|list:
|
|
11
|
+
def get(cwd: bool = False, base_dir: bool = False) -> str | list:
|
|
14
12
|
paths = []
|
|
15
13
|
if cwd:
|
|
16
14
|
paths.append(_os.getcwd())
|
|
17
15
|
if base_dir:
|
|
18
|
-
if getattr(_sys,
|
|
16
|
+
if getattr(_sys, "frozen", False):
|
|
19
17
|
base_path = _os.path.dirname(_sys.executable)
|
|
20
18
|
else:
|
|
21
|
-
main_module = _sys.modules[
|
|
22
|
-
if hasattr(main_module,
|
|
19
|
+
main_module = _sys.modules["__main__"]
|
|
20
|
+
if hasattr(main_module, "__file__"):
|
|
23
21
|
base_path = _os.path.dirname(_os.path.abspath(main_module.__file__))
|
|
24
|
-
elif
|
|
22
|
+
elif (
|
|
23
|
+
hasattr(main_module, "__spec__") and main_module.__spec__ and getattr(main_module.__spec__, "origin", None)
|
|
24
|
+
):
|
|
25
25
|
base_path = _os.path.dirname(_os.path.abspath(main_module.__spec__.origin))
|
|
26
26
|
else:
|
|
27
|
-
raise RuntimeError(
|
|
27
|
+
raise RuntimeError("Can only get base directory if ran from a file.")
|
|
28
28
|
paths.append(base_path)
|
|
29
29
|
return paths[0] if len(paths) == 1 else paths
|
|
30
30
|
|
|
31
31
|
@staticmethod
|
|
32
|
-
def extend(
|
|
33
|
-
|
|
32
|
+
def extend(
|
|
33
|
+
path: str,
|
|
34
|
+
search_in: str | list[str] = None,
|
|
35
|
+
raise_error: bool = False,
|
|
36
|
+
correct_path: bool = False,
|
|
37
|
+
) -> str:
|
|
38
|
+
if path in (None, ""):
|
|
34
39
|
return path
|
|
35
|
-
|
|
40
|
+
|
|
41
|
+
def get_closest_match(dir: str, part: str) -> str | None:
|
|
36
42
|
try:
|
|
37
43
|
files_and_dirs = _os.listdir(dir)
|
|
38
44
|
matches = _difflib.get_close_matches(part, files_and_dirs, n=1, cutoff=0.6)
|
|
39
45
|
return matches[0] if matches else None
|
|
40
|
-
except:
|
|
46
|
+
except Exception:
|
|
41
47
|
return None
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
def find_path(start: str, parts: list[str]) -> str | None:
|
|
43
50
|
current = start
|
|
44
51
|
for part in parts:
|
|
45
52
|
if _os.path.isfile(current):
|
|
@@ -49,23 +56,30 @@ class Path:
|
|
|
49
56
|
if current is None:
|
|
50
57
|
return None
|
|
51
58
|
return current if _os.path.exists(current) and current != start else None
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
def expand_env_path(p: str) -> str:
|
|
61
|
+
if "%" not in p:
|
|
54
62
|
return p
|
|
55
|
-
parts = p.split(
|
|
63
|
+
parts = p.split("%")
|
|
56
64
|
for i in range(1, len(parts), 2):
|
|
57
65
|
if parts[i].upper() in _os.environ:
|
|
58
66
|
parts[i] = _os.environ[parts[i].upper()]
|
|
59
|
-
return
|
|
67
|
+
return "".join(parts)
|
|
68
|
+
|
|
60
69
|
path = _os.path.normpath(expand_env_path(path))
|
|
61
70
|
if _os.path.isabs(path):
|
|
62
71
|
drive, rel_path = _os.path.splitdrive(path)
|
|
63
72
|
rel_path = rel_path.lstrip(_os.sep)
|
|
64
|
-
search_dirs =
|
|
73
|
+
search_dirs = (drive + _os.sep) if drive else [_os.sep]
|
|
65
74
|
else:
|
|
66
75
|
rel_path = path.lstrip(_os.sep)
|
|
67
76
|
base_dir = Path.get(base_dir=True)
|
|
68
|
-
search_dirs =
|
|
77
|
+
search_dirs = (
|
|
78
|
+
_os.getcwd(),
|
|
79
|
+
base_dir,
|
|
80
|
+
_os.path.expanduser("~"),
|
|
81
|
+
_tempfile.gettempdir(),
|
|
82
|
+
)
|
|
69
83
|
if search_in:
|
|
70
84
|
search_dirs.extend([search_in] if isinstance(search_in, str) else search_in)
|
|
71
85
|
path_parts = rel_path.split(_os.sep)
|
|
@@ -74,13 +88,14 @@ class Path:
|
|
|
74
88
|
if _os.path.exists(full_path):
|
|
75
89
|
return full_path
|
|
76
90
|
match = find_path(search_dir, path_parts) if correct_path else None
|
|
77
|
-
if match:
|
|
91
|
+
if match:
|
|
92
|
+
return match
|
|
78
93
|
if raise_error:
|
|
79
|
-
raise FileNotFoundError(f
|
|
94
|
+
raise FileNotFoundError(f"Path '{path}' not found in specified directories.")
|
|
80
95
|
return _os.path.join(search_dirs[0], rel_path)
|
|
81
96
|
|
|
82
97
|
@staticmethod
|
|
83
|
-
def remove(path:str, only_content:bool = False) -> None:
|
|
98
|
+
def remove(path: str, only_content: bool = False) -> None:
|
|
84
99
|
if not _os.path.exists(path):
|
|
85
100
|
return None
|
|
86
101
|
if not only_content:
|
|
@@ -94,4 +109,4 @@ class Path:
|
|
|
94
109
|
elif _os.path.isdir(file_path):
|
|
95
110
|
_shutil.rmtree(file_path)
|
|
96
111
|
except Exception as e:
|
|
97
|
-
raise Exception(f
|
|
112
|
+
raise Exception(f"Failed to delete {file_path}. Reason: {e}")
|