xulbux 1.6.8__py3-none-any.whl → 1.7.0__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_console.py CHANGED
@@ -8,10 +8,10 @@ For more detailed information about formatting codes, see the the `xx_format_cod
8
8
  from ._consts_ import COLOR, CHARS
9
9
  from .xx_format_codes import FormatCodes, _COMPILED
10
10
  from .xx_string import String
11
- from .xx_color import Color, rgba, hexa
11
+ from .xx_color import Color, Rgba, Hexa
12
12
 
13
13
  from prompt_toolkit.key_binding.key_bindings import KeyBindings
14
- from typing import Optional
14
+ from typing import Optional, Any
15
15
  import prompt_toolkit as _prompt_toolkit
16
16
  import pyperclip as _pyperclip
17
17
  import keyboard as _keyboard
@@ -22,106 +22,142 @@ import sys as _sys
22
22
  import os as _os
23
23
 
24
24
 
25
- # YAPF: disable
26
25
  class _ConsoleWidth:
26
+
27
27
  def __get__(self, obj, owner=None):
28
28
  return _os.get_terminal_size().columns
29
29
 
30
+
30
31
  class _ConsoleHeight:
32
+
31
33
  def __get__(self, obj, owner=None):
32
34
  return _os.get_terminal_size().lines
33
35
 
36
+
34
37
  class _ConsoleSize:
38
+
35
39
  def __get__(self, obj, owner=None):
36
40
  size = _os.get_terminal_size()
37
41
  return (size.columns, size.lines)
38
42
 
43
+
39
44
  class _ConsoleUser:
45
+
40
46
  def __get__(self, obj, owner=None):
41
47
  return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
42
48
 
49
+
43
50
  class ArgResult:
44
51
  """Exists: if the argument was found or not\n
45
52
  Value: the value from behind the found argument"""
46
- def __init__(self, exists: bool, value: any):
53
+
54
+ def __init__(self, exists: bool, value: Any):
47
55
  self.exists = exists
48
56
  self.value = value
57
+
49
58
  def __bool__(self):
50
59
  return self.exists
51
60
 
61
+
52
62
  class Args:
53
63
  """Stores found command arguments under their aliases with their results."""
64
+
54
65
  def __init__(self, **kwargs):
55
66
  for key, value in kwargs.items():
56
67
  if not key.isidentifier():
57
68
  raise TypeError(f"Argument alias '{key}' is invalid. It must be a valid Python variable name.")
58
69
  setattr(self, key, ArgResult(**value))
70
+
59
71
  def __len__(self):
60
72
  return len(vars(self))
73
+
61
74
  def __contains__(self, key):
62
75
  return hasattr(self, key)
76
+
63
77
  def __getitem__(self, key):
64
78
  if isinstance(key, int):
65
79
  return list(self.__iter__())[key]
66
80
  return getattr(self, key)
81
+
67
82
  def __iter__(self):
68
83
  for key, value in vars(self).items():
69
84
  yield (key, {"exists": value.exists, "value": value.value})
70
- def dict(self) -> dict[str, dict[str, any]]:
85
+
86
+ def dict(self) -> dict[str, dict[str, Any]]:
71
87
  """Returns the arguments as a dictionary."""
72
88
  return {k: {"exists": v.exists, "value": v.value} for k, v in vars(self).items()}
89
+
73
90
  def keys(self):
74
91
  """Returns the argument aliases as `dict_keys([...])`."""
75
92
  return vars(self).keys()
93
+
76
94
  def values(self):
77
95
  """Returns the argument results as `dict_values([...])`."""
78
96
  return vars(self).values()
97
+
79
98
  def items(self):
80
- """Returns the argument aliases and results as `dict_items([...])`."""
81
- return vars(self).items()
82
- # YAPF: enable
99
+ """Yields tuples of `(alias, {'exists': bool, 'value': Any})`."""
100
+ for key, value in self.__iter__():
101
+ yield (key, value)
83
102
 
84
103
 
85
104
  class Console:
86
105
 
87
- w: int = _ConsoleWidth()
106
+ w: int = _ConsoleWidth() # type: ignore[assignment]
88
107
  """The width of the console in characters."""
89
- h: int = _ConsoleHeight()
108
+ h: int = _ConsoleHeight() # type: ignore[assignment]
90
109
  """The height of the console in lines."""
91
- wh: tuple[int, int] = _ConsoleSize()
110
+ wh: tuple[int, int] = _ConsoleSize() # type: ignore[assignment]
92
111
  """A tuple with the width and height of
93
112
  the console in characters and lines."""
94
- usr: str = _ConsoleUser()
113
+ usr: str = _ConsoleUser() # type: ignore[assignment]
95
114
  """The name of the current user."""
96
115
 
97
116
  @staticmethod
98
- def get_args(find_args: dict[str, list[str] | tuple[str, ...]], allow_spaces: bool = False) -> Args:
117
+ def get_args(
118
+ find_args: dict[str, list[str] | tuple[str, ...] | dict[str, list[str] | tuple[str, ...] | Any]],
119
+ allow_spaces: bool = False
120
+ ) -> Args:
99
121
  """Will search for the specified arguments in the command line
100
122
  arguments and return the results as a special `Args` object.\n
101
123
  ----------------------------------------------------------------
102
- The `find_args` dictionary should have the following structure:
124
+ The `find_args` dictionary can have the following structures for each alias:
125
+ 1. Simple list/tuple of flags (when no default value is needed):
126
+ ```python
127
+ "alias_name": ["-f", "--flag"]
128
+ ```
129
+ 2. Dictionary with 'flags' and optional 'default':
130
+ ```python
131
+ "alias_name": {
132
+ "flags": ["-f", "--flag"],
133
+ "default": "some_value" # Optional
134
+ }
135
+ ```
136
+ Example `find_args`:
103
137
  ```python
104
138
  find_args={
105
- "arg1_alias": ["-a1", "--arg1", "--argument-1"],
106
- "arg2_alias": ("-a2", "--arg2", "--argument-2"),
107
- ...
139
+ "arg1": { # With default
140
+ "flags": ["-a1", "--arg1"],
141
+ "default": "default_val"
142
+ },
143
+ "arg2": ("-a2", "--arg2"), # Without default (original format)
144
+ "arg3": ["-a3"], # Without default (list format)
145
+ "arg4": { # Flag with default True
146
+ "flags": ["-f"],
147
+ "default": True
148
+ }
108
149
  }
109
150
  ```
110
- And if the script is called via the command line:\n
111
- `python script.py -a1 "argument value" --arg2`\n
112
- ...it would return the following `Args` object:
113
- ```python
114
- Args(
115
- arg1_alias=ArgResult(exists=True, value="argument value"),
116
- arg2_alias=ArgResult(exists=True, value=None),
117
- ...
118
- )
119
- ```
120
- ...which can be accessed like this:\n
121
- - `Args.<arg_alias>.exists` is `True` if any of the specified
122
- args were found and `False` if not
123
- - `Args.<arg_alias>.value` the value from behind the found arg,
124
- `None` if no value was found\n
151
+ If the script is called via the command line:\n
152
+ `python script.py -a1 "value1" --arg2 -f`\n
153
+ ...it would return an `Args` object where:
154
+ - `args.arg1.exists` is `True`, `args.arg1.value` is `"value1"`
155
+ - `args.arg2.exists` is `True`, `args.arg2.value` is `True` (flag present without value)
156
+ - `args.arg3.exists` is `False`, `args.arg3.value` is `None` (not present, no default)
157
+ - `args.arg4.exists` is `True`, `args.arg4.value` is `True` (flag present, overrides default)
158
+ - If an arg defined in `find_args` is *not* present in the command line:
159
+ - `exists` will be `False`
160
+ - `value` will be the specified `default` value, or `None` if no default was specified.\n
125
161
  ----------------------------------------------------------------
126
162
  Normally if `allow_spaces` is false, it will take a space as
127
163
  the end of an args value. If it is true, it will take spaces as
@@ -130,20 +166,40 @@ class Console:
130
166
  args = _sys.argv[1:]
131
167
  args_len = len(args)
132
168
  arg_lookup = {}
133
- for arg_key, arg_group in find_args.items():
134
- for arg in arg_group:
135
- arg_lookup[arg] = arg_key
136
- results = {key: {"exists": False, "value": None} for key in find_args}
169
+ results = {}
170
+ for alias, config in find_args.items():
171
+ flags = None
172
+ default_value = None
173
+ if isinstance(config, (list, tuple)):
174
+ flags = config
175
+ elif isinstance(config, dict):
176
+ if "flags" not in config:
177
+ raise ValueError(f"Invalid configuration for alias '{alias}'. Dictionary must contain a 'flags' key.")
178
+ flags = config["flags"]
179
+ default_value = config.get("default")
180
+ if not isinstance(flags, (list, tuple)):
181
+ raise ValueError(f"Invalid 'flags' for alias '{alias}'. Must be a list or tuple.")
182
+ else:
183
+ raise TypeError(f"Invalid configuration type for alias '{alias}'. Must be a list, tuple, or dict.")
184
+ results[alias] = {"exists": False, "value": default_value}
185
+ for flag in flags:
186
+ if flag in arg_lookup:
187
+ raise ValueError(
188
+ f"Duplicate flag '{flag}' found. It's assigned to both '{arg_lookup[flag]}' and '{alias}'."
189
+ )
190
+ arg_lookup[flag] = alias
137
191
  i = 0
138
192
  while i < args_len:
139
193
  arg = args[i]
140
- arg_key = arg_lookup.get(arg)
141
- if arg_key:
142
- results[arg_key]["exists"] = True
194
+ alias = arg_lookup.get(arg)
195
+ if alias:
196
+ results[alias]["exists"] = True
197
+ value_found_after_flag = False
143
198
  if i + 1 < args_len and not args[i + 1].startswith("-"):
144
199
  if not allow_spaces:
145
- results[arg_key]["value"] = String.to_type(args[i + 1])
200
+ results[alias]["value"] = String.to_type(args[i + 1])
146
201
  i += 1
202
+ value_found_after_flag = True
147
203
  else:
148
204
  value_parts = []
149
205
  j = i + 1
@@ -151,8 +207,11 @@ class Console:
151
207
  value_parts.append(args[j])
152
208
  j += 1
153
209
  if value_parts:
154
- results[arg_key]["value"] = String.to_type(" ".join(value_parts))
210
+ results[alias]["value"] = String.to_type(" ".join(value_parts))
155
211
  i = j - 1
212
+ value_found_after_flag = True
213
+ if not value_found_after_flag:
214
+ results[alias]["value"] = True
156
215
  i += 1
157
216
  return Args(**results)
158
217
 
@@ -190,8 +249,8 @@ class Console:
190
249
  format_linebreaks: bool = True,
191
250
  start: str = "",
192
251
  end: str = "\n",
193
- title_bg_color: hexa | rgba = None,
194
- default_color: hexa | rgba = None,
252
+ title_bg_color: Optional[Rgba | Hexa] = None,
253
+ default_color: Optional[Rgba | Hexa] = None,
195
254
  _console_tabsize: int = 8,
196
255
  ) -> None:
197
256
  """Will print a formatted log message:
@@ -210,10 +269,14 @@ class Console:
210
269
  title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
211
270
  title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
212
271
  if format_linebreaks:
213
- clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True)
272
+ clean_prompt, removals = FormatCodes.remove_formatting(str(prompt), get_removals=True, _ignore_linebreaks=True)
214
273
  prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(clean_prompt).splitlines())
215
- prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
216
- prompt = f"\n{' ' * title_len}\t".join(Console.__add_back_removed_parts(list(prompt_lst), removals))
274
+ prompt_lst = (
275
+ item for lst in prompt_lst for item in ([""] if lst == [] else (lst if isinstance(lst, list) else [lst]))
276
+ )
277
+ prompt = f"\n{' ' * title_len}\t".join(
278
+ Console.__add_back_removed_parts(list(prompt_lst), removals) # type: ignore[assignment]
279
+ )
217
280
  else:
218
281
  prompt = str(prompt)
219
282
  if title == "":
@@ -269,8 +332,8 @@ class Console:
269
332
  format_linebreaks: bool = True,
270
333
  start: str = "",
271
334
  end: str = "\n",
272
- title_bg_color: hexa | rgba = COLOR.yellow,
273
- default_color: hexa | rgba = COLOR.text,
335
+ title_bg_color: Rgba | Hexa = COLOR.yellow,
336
+ default_color: Rgba | Hexa = COLOR.text,
274
337
  pause: bool = False,
275
338
  exit: bool = False,
276
339
  ) -> None:
@@ -287,8 +350,8 @@ class Console:
287
350
  format_linebreaks: bool = True,
288
351
  start: str = "",
289
352
  end: str = "\n",
290
- title_bg_color: hexa | rgba = COLOR.blue,
291
- default_color: hexa | rgba = COLOR.text,
353
+ title_bg_color: Rgba | Hexa = COLOR.blue,
354
+ default_color: Rgba | Hexa = COLOR.text,
292
355
  pause: bool = False,
293
356
  exit: bool = False,
294
357
  ) -> None:
@@ -303,8 +366,8 @@ class Console:
303
366
  format_linebreaks: bool = True,
304
367
  start: str = "",
305
368
  end: str = "\n",
306
- title_bg_color: hexa | rgba = COLOR.teal,
307
- default_color: hexa | rgba = COLOR.text,
369
+ title_bg_color: Rgba | Hexa = COLOR.teal,
370
+ default_color: Rgba | Hexa = COLOR.text,
308
371
  pause: bool = False,
309
372
  exit: bool = False,
310
373
  ) -> None:
@@ -319,8 +382,8 @@ class Console:
319
382
  format_linebreaks: bool = True,
320
383
  start: str = "",
321
384
  end: str = "\n",
322
- title_bg_color: hexa | rgba = COLOR.orange,
323
- default_color: hexa | rgba = COLOR.text,
385
+ title_bg_color: Rgba | Hexa = COLOR.orange,
386
+ default_color: Rgba | Hexa = COLOR.text,
324
387
  pause: bool = False,
325
388
  exit: bool = False,
326
389
  ) -> None:
@@ -335,8 +398,8 @@ class Console:
335
398
  format_linebreaks: bool = True,
336
399
  start: str = "",
337
400
  end: str = "\n",
338
- title_bg_color: hexa | rgba = COLOR.red,
339
- default_color: hexa | rgba = COLOR.text,
401
+ title_bg_color: Rgba | Hexa = COLOR.red,
402
+ default_color: Rgba | Hexa = COLOR.text,
340
403
  pause: bool = False,
341
404
  exit: bool = True,
342
405
  reset_ansi=True,
@@ -352,8 +415,8 @@ class Console:
352
415
  format_linebreaks: bool = True,
353
416
  start: str = "",
354
417
  end: str = "\n",
355
- title_bg_color: hexa | rgba = COLOR.magenta,
356
- default_color: hexa | rgba = COLOR.text,
418
+ title_bg_color: Rgba | Hexa = COLOR.magenta,
419
+ default_color: Rgba | Hexa = COLOR.text,
357
420
  pause: bool = False,
358
421
  exit: bool = True,
359
422
  reset_ansi=True,
@@ -368,8 +431,8 @@ class Console:
368
431
  *values: object,
369
432
  start: str = "",
370
433
  end: str = "\n",
371
- box_bg_color: str | hexa | rgba = "green",
372
- default_color: hexa | rgba = "#000",
434
+ box_bg_color: str | Rgba | Hexa = "green",
435
+ default_color: Rgba | Hexa = "#000",
373
436
  w_padding: int = 2,
374
437
  w_full: bool = False,
375
438
  ) -> None:
@@ -384,7 +447,7 @@ class Console:
384
447
  -----------------------------------------------------------------------------------
385
448
  The box content can be formatted with special formatting codes. For more detailed
386
449
  information about formatting codes, see `xx_format_codes` module documentation."""
387
- lines = [line.strip() for val in values for line in val.splitlines()]
450
+ lines = [line.rstrip() for val in values for line in str(val).splitlines()]
388
451
  unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
389
452
  max_line_len = max(len(line) for line in unfmt_lines)
390
453
  pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
@@ -407,7 +470,7 @@ class Console:
407
470
  prompt: object = "Do you want to continue?",
408
471
  start="",
409
472
  end="\n",
410
- default_color: hexa | rgba = COLOR.cyan,
473
+ default_color: Rgba | Hexa = COLOR.cyan,
411
474
  default_is_yes: bool = True,
412
475
  ) -> bool:
413
476
  """Ask a yes/no question.\n
@@ -429,7 +492,7 @@ class Console:
429
492
  prompt: object = "",
430
493
  start="",
431
494
  end="\n",
432
- default_color: hexa | rgba = COLOR.cyan,
495
+ default_color: Rgba | Hexa = COLOR.cyan,
433
496
  show_keybindings=True,
434
497
  input_prefix=" ⤷ ",
435
498
  reset_ansi=True,
@@ -452,7 +515,7 @@ class Console:
452
515
  def _(event):
453
516
  event.app.exit(result=event.app.current_buffer.document.text)
454
517
 
455
- FormatCodes.print(start + prompt, default_color=default_color)
518
+ FormatCodes.print(start + str(prompt), default_color=default_color)
456
519
  if show_keybindings:
457
520
  FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
458
521
  input_string = _prompt_toolkit.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
@@ -464,11 +527,11 @@ class Console:
464
527
  prompt: object = "",
465
528
  start="",
466
529
  end="\n",
467
- default_color: hexa | rgba = COLOR.cyan,
468
- allowed_chars: str = CHARS.all,
469
- min_len: int = None,
470
- max_len: int = None,
471
- mask_char: str = None,
530
+ default_color: Rgba | Hexa = COLOR.cyan,
531
+ allowed_chars: str = CHARS.all, # type: ignore[assignment]
532
+ min_len: Optional[int] = None,
533
+ max_len: Optional[int] = None,
534
+ mask_char: Optional[str] = None,
472
535
  reset_ansi: bool = True,
473
536
  ) -> Optional[str]:
474
537
  """Acts like a standard Python `input()` with the advantage, that you can specify:
@@ -479,14 +542,14 @@ class Console:
479
542
  ---------------------------------------------------------------------------------------
480
543
  The input can be formatted with special formatting codes. For more detailed
481
544
  information about formatting codes, see the `xx_format_codes` module documentation."""
482
- FormatCodes.print(start + prompt, default_color=default_color, end="")
545
+ FormatCodes.print(start + str(prompt), default_color=default_color, end="")
483
546
  result = ""
484
547
  select_all = False
485
548
  last_line_count = 1
486
549
  last_console_width = 0
487
550
 
488
551
  def update_display(console_width: int) -> None:
489
- nonlocal select_all, last_line_count, last_console_width
552
+ nonlocal last_line_count, last_console_width
490
553
  lines = String.split_count(str(prompt) + (mask_char * len(result) if mask_char else result), console_width)
491
554
  line_count = len(lines)
492
555
  if (line_count > 1 or line_count < last_line_count) and not last_line_count == 1:
@@ -535,7 +598,8 @@ class Console:
535
598
 
536
599
  def handle_character_input():
537
600
  nonlocal result
538
- if (allowed_chars == CHARS.all or event.name in allowed_chars) and (max_len is None or len(result) < max_len):
601
+ if event.name is not None and ((allowed_chars == CHARS.all or event.name in allowed_chars) and
602
+ (max_len is None or len(result) < max_len)):
539
603
  result += event.name
540
604
  update_display(Console.w)
541
605
 
@@ -556,7 +620,7 @@ class Console:
556
620
  return None
557
621
  elif event.name == "space":
558
622
  handle_character_input()
559
- elif len(event.name) == 1:
623
+ elif event.name is not None and len(event.name) == 1:
560
624
  handle_character_input()
561
625
  else:
562
626
  select_all = False
@@ -567,12 +631,22 @@ class Console:
567
631
  prompt: object = "Password: ",
568
632
  start="",
569
633
  end="\n",
570
- default_color: hexa | rgba = COLOR.cyan,
634
+ default_color: Rgba | Hexa = COLOR.cyan,
571
635
  allowed_chars: str = CHARS.standard_ascii,
572
- min_len: int = None,
573
- max_len: int = None,
636
+ min_len: Optional[int] = None,
637
+ max_len: Optional[int] = None,
574
638
  reset_ansi: bool = True,
575
- ) -> str:
639
+ ) -> Optional[str]:
576
640
  """Password input (preset for `Console.restricted_input()`)
577
641
  that always masks the entered characters with asterisks."""
578
- return Console.restricted_input(prompt, start, end, default_color, allowed_chars, min_len, max_len, "*", reset_ansi)
642
+ return Console.restricted_input(
643
+ prompt=prompt,
644
+ start=start,
645
+ end=end,
646
+ default_color=default_color,
647
+ allowed_chars=allowed_chars,
648
+ min_len=min_len,
649
+ max_len=max_len,
650
+ mask_char="*",
651
+ reset_ansi=reset_ansi,
652
+ )