untils 1.0.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.
untils/input_token.py ADDED
@@ -0,0 +1,104 @@
1
+ """input_token.py - Raw and Final input tokens for `Tokenizer` and `InputValidator`."""
2
+
3
+ # pylint: disable=too-few-public-methods
4
+
5
+ from typing import Any, Literal
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from untils.utils.enums import RawTokenType, FinalTokenType
10
+
11
+ @dataclass(frozen=True)
12
+ class RawInputToken:
13
+ """The raw input token for `Tokenizer`."""
14
+
15
+ type: RawTokenType
16
+ """The raw command type."""
17
+ value: str
18
+ """The raw value."""
19
+
20
+ def __repr__(self) -> str:
21
+ return f"RawInputToken[{self.type.name}](value='{self.value}')"
22
+
23
+
24
+
25
+ class FinalInputTokenWord:
26
+ """The word type of `FinalInputToken`."""
27
+
28
+ type: Literal[FinalTokenType.WORD]
29
+ """The command type. Is literal."""
30
+ value: str
31
+ """The command value."""
32
+
33
+ def __init__(self, value: str) -> None:
34
+ self.type = FinalTokenType.WORD
35
+ self.value = value
36
+
37
+ def __repr__(self) -> str:
38
+ return f"FinalInputTokenWord(value={self.value})"
39
+
40
+ def __eq__(self, value: object) -> bool:
41
+ if isinstance(value, FinalInputTokenWord):
42
+ return self.value == value.value
43
+ return False
44
+
45
+ def __ne__(self, value: object) -> bool:
46
+ if isinstance(value, FinalInputTokenWord):
47
+ return self.value != value.value
48
+ return True
49
+
50
+ class FinalInputTokenFlag:
51
+ """The flag type of `FinalInputToken`."""
52
+
53
+ type: Literal[FinalTokenType.FLAG]
54
+ """The command type. Is literal."""
55
+ name: str
56
+ """The command name."""
57
+ value: bool
58
+ """The command value."""
59
+
60
+ def __init__(self, name: str, value: bool) -> None:
61
+ self.type = FinalTokenType.FLAG
62
+ self.name = name
63
+ self.value = value
64
+
65
+ def __repr__(self) -> str:
66
+ return f"FinalInputTokenFlag(name={self.name}, value={self.value})"
67
+
68
+ def __eq__(self, value: object) -> bool:
69
+ if isinstance(value, FinalInputTokenFlag):
70
+ return self.name == value.name and self.value == value.value
71
+ return False
72
+
73
+ def __ne__(self, value: object) -> bool:
74
+ if isinstance(value, FinalInputTokenFlag):
75
+ return self.name != value.name or self.value != value.value
76
+ return True
77
+
78
+ class FinalInputTokenOption:
79
+ """The option type of `FinalInputToken`."""
80
+
81
+ type: Literal[FinalTokenType.OPTION]
82
+ """The command type. Is literal."""
83
+ name: str
84
+ """The command name."""
85
+ value: Any
86
+ """The command value."""
87
+
88
+ def __init__(self, name: str, value: Any) -> None:
89
+ self.type = FinalTokenType.OPTION
90
+ self.name = name
91
+ self.value = value
92
+
93
+ def __repr__(self) -> str:
94
+ return f"FinalInputTokenOption(name={self.name}, value={self.value})"
95
+
96
+ def __eq__(self, value: object) -> bool:
97
+ if isinstance(value, FinalInputTokenOption):
98
+ return self.name == value.name and self.value == value.value
99
+ return False
100
+
101
+ def __ne__(self, value: object) -> bool:
102
+ if isinstance(value, FinalInputTokenOption):
103
+ return self.name != value.name or self.value != value.value
104
+ return True
@@ -0,0 +1,556 @@
1
+ """input_validator.py - Input validations."""
2
+
3
+ from typing import List, Literal, cast, Union, Dict, Optional
4
+
5
+ from untils.utils.type_aliases import InputDict
6
+ from untils.utils.enums import RawTokenType, FinalTokenType, InternalState
7
+ from untils.utils.constants import Strings
8
+ from untils.utils.protocols import FinalInputProtocol
9
+
10
+ from untils.input_token import (
11
+ RawInputToken, FinalInputTokenWord, FinalInputTokenFlag,
12
+ FinalInputTokenOption
13
+ )
14
+ from untils.settings import Settings
15
+ from untils.utils.lib_warnings import (
16
+ InputStructureWarning, InputValuesWarning, InputStructureError, InputValuesError
17
+ )
18
+ from untils.commands_config import CommandsConfig
19
+ from untils.command import (
20
+ CommandNode, CommandWordNode, CommandFallbackNode, CommandFlagNode, CommandOptionNode
21
+ )
22
+
23
+ class InputValidator:
24
+ """Validator class for tokenized input."""
25
+
26
+ __slots__ = ["_settings", "_config", "_input_tokens", "_result", "_i"]
27
+
28
+ _settings: Settings
29
+ """The settings."""
30
+ _config: Optional[CommandsConfig]
31
+ """The commands config."""
32
+ _input_tokens: List[RawInputToken]
33
+ """The raw input tokens."""
34
+ _result: List[FinalInputProtocol]
35
+ """The result of final input tokens."""
36
+ _i: int
37
+ """The validation index."""
38
+
39
+ def __init__(
40
+ self,
41
+ settings: Settings,
42
+ config: Optional[CommandsConfig],
43
+ input_tokens: List[RawInputToken]
44
+ ) -> None:
45
+ """
46
+ Args:
47
+ settings: The settings.
48
+ config: The validated and parsed commands config.
49
+ input_tokens: The raw input tokens.
50
+ """
51
+
52
+ self._settings = settings
53
+ self._config = config
54
+ self._input_tokens = input_tokens
55
+ self._result = []
56
+ self._i = 0
57
+
58
+ def warning_out_of_bounce(self) -> None:
59
+ """Warnings out of bounce.
60
+
61
+ Raises:
62
+ InputStructureWarning: Always.
63
+
64
+ InputStructureError: Always.
65
+ """
66
+
67
+ self._settings.warning(
68
+ Strings.END_OF_INPUT,
69
+ Strings.AUTO_CORRECT_WITH_REMOVING,
70
+ InputStructureWarning,
71
+ InputStructureError
72
+ )
73
+
74
+ def cast_token(
75
+ self,
76
+ token_object: Union[FinalInputTokenWord, FinalInputTokenFlag, FinalInputTokenOption]
77
+ ) -> FinalInputProtocol:
78
+ """Casts a token to the `FinalInputProtocol`.
79
+
80
+ Args:
81
+ token_object: The token.
82
+
83
+ Returns:
84
+ The casted token.
85
+ """
86
+
87
+ return cast(FinalInputProtocol, token_object)
88
+
89
+ def expect_end(self, offset: int=1) -> None:
90
+ """Expects an end by offset.
91
+
92
+ Args:
93
+ offset: The number greater than 0, which defines an index offset, that cannot be out of bounce.
94
+
95
+ Raises:
96
+ InputStructureWarning: If the offset out of bounce.
97
+
98
+ InputStructureError: If the offset out of bounce.
99
+ """
100
+
101
+ if offset <= 0:
102
+ self._settings.warning(
103
+ Strings.NON_POSITIVE_OFFSET,
104
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
105
+ InputValuesWarning,
106
+ InputValuesError
107
+ )
108
+ return
109
+
110
+ if self._i >= len(self._input_tokens) - offset:
111
+ self._settings.warning(
112
+ Strings.END_OF_INPUT,
113
+ Strings.AUTO_CORRECT_WITH_ACCEPTING,
114
+ InputStructureWarning,
115
+ InputStructureError
116
+ )
117
+
118
+ def validate_token_word(self) -> None:
119
+ """Validates a `Word` token."""
120
+
121
+ token: RawInputToken = self._input_tokens[self._i]
122
+ self._result.append(self.cast_token(FinalInputTokenWord(token.value)))
123
+ self._i += 1
124
+
125
+ def validate_token_flag(self) -> None:
126
+ """Validates a `Flag` token."""
127
+
128
+ value: bool = True
129
+
130
+ invert_token: RawInputToken = self._input_tokens[self._i]
131
+ if invert_token.type == RawTokenType.NOT:
132
+ # Invert mark.
133
+ self._settings.logger.debug("Process `Not` token.")
134
+
135
+ value = False
136
+ self.expect_end(offset=1) # [...][CURRENT_TOKEN][!LOOKUP!][...]
137
+ self._i += 1
138
+
139
+ if self._input_tokens[self._i].type != RawTokenType.WORD:
140
+ self._settings.warning(
141
+ Strings.EXPECTED_SYNTAX_FLAG,
142
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
143
+ InputStructureWarning,
144
+ InputStructureError
145
+ )
146
+ while (
147
+ self._i <= len(self._input_tokens)
148
+ and self._input_tokens[self._i].type != RawTokenType.WORD
149
+ ):
150
+ self._i += 1
151
+
152
+ name_tokens: List[RawInputToken] = self.validate_name_tokens()
153
+ name: str = ""
154
+
155
+ for name_token in name_tokens:
156
+ name += name_token.value
157
+
158
+ if name != "":
159
+ # The flag's name.
160
+ self._settings.logger.debug("Process `Word` token for the flag's name.")
161
+ self._result.append(self.cast_token(FinalInputTokenFlag(name, value)))
162
+ else:
163
+ self._settings.warning(
164
+ Strings.EXPECTED_SYNTAX_FLAG,
165
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
166
+ InputStructureWarning,
167
+ InputStructureError
168
+ )
169
+
170
+ def validate_name_tokens(self) -> List[RawInputToken]:
171
+ """Validates a row of tokens with type `Word` and `String` without spaces as name.
172
+
173
+ Returns:
174
+ The list of name tokens.
175
+ """
176
+
177
+ self._settings.logger.debug("Process name tokens.")
178
+
179
+ current_token: RawInputToken = self._input_tokens[self._i]
180
+ name_tokens: List[RawInputToken] = [current_token]
181
+
182
+ if current_token.type == RawTokenType.STRING:
183
+ return name_tokens
184
+ if current_token.type not in (RawTokenType.WORD, RawTokenType.MINUS):
185
+ self._settings.warning(
186
+ Strings.COMMAND_UNKNOWN_NAME,
187
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
188
+ InputStructureWarning,
189
+ InputStructureError
190
+ )
191
+ self._i += 1
192
+ return name_tokens
193
+
194
+ if self._i == len(self._input_tokens) - 1:
195
+ return name_tokens
196
+
197
+ self.expect_end(offset=1) # [...][CURRENT_TOKEN][!LOOKUP!][...]
198
+ self._i += 1
199
+
200
+ while (
201
+ (self._i < len(self._input_tokens))
202
+ and (self._input_tokens[self._i].type in (RawTokenType.WORD, RawTokenType.MINUS))
203
+ ):
204
+ self._settings.logger.debug(f"Process name token: {self._input_tokens[self._i]}.")
205
+ name_tokens.append(self._input_tokens[self._i])
206
+ self._i += 1
207
+
208
+ self._settings.logger.debug(f"Processed name tokens: {name_tokens}.")
209
+
210
+ return name_tokens
211
+
212
+ def validate_token_option(self) -> None:
213
+ """Validates an `Option` token."""
214
+
215
+ name_tokens: List[RawInputToken] = self.validate_name_tokens()
216
+ name: str = ""
217
+
218
+ for name_token in name_tokens:
219
+ name += name_token.value
220
+
221
+ if name == "":
222
+ self._settings.warning(
223
+ Strings.OPTION_NAME_INVALID,
224
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
225
+ InputStructureWarning,
226
+ InputStructureError
227
+ )
228
+
229
+ self.expect_end(offset=1) # [...][CURRENT_TOKEN][!LOOKUP!][...]
230
+ self._i += 1
231
+
232
+ while (
233
+ self._i < len(self._input_tokens)
234
+ and self._input_tokens[self._i].type == RawTokenType.SPACE
235
+ ):
236
+ self._i += 1
237
+
238
+ value_tokens: List[RawInputToken] = self.validate_name_tokens()
239
+ value: str = ""
240
+
241
+ for value_token in value_tokens:
242
+ value += value_token.value
243
+
244
+ if value != "":
245
+ # The option's value.
246
+ self._settings.logger.debug("Process `Word` or `String` token for the option's value")
247
+ self._result.append(self.cast_token(FinalInputTokenOption(name, value)))
248
+ else:
249
+ self._settings.warning(
250
+ Strings.OPTION_VALUE_INVALID,
251
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
252
+ InputStructureWarning,
253
+ InputStructureError
254
+ )
255
+
256
+ def validate_token_minus(self) -> None:
257
+ """Validates a `Minus` token."""
258
+
259
+ start: int = self._i
260
+
261
+ while (
262
+ self._i < len(self._input_tokens)
263
+ and self._input_tokens[self._i].type == RawTokenType.MINUS
264
+ ):
265
+ self._i += 1
266
+
267
+ count: int = self._i - start
268
+ expected_type: Literal[FinalTokenType.FLAG, FinalTokenType.OPTION]\
269
+ = FinalTokenType.FLAG if count == 1 else FinalTokenType.OPTION
270
+
271
+ if expected_type == FinalTokenType.FLAG:
272
+ self._settings.logger.debug("Expected `Flag` construction.")
273
+ self.validate_token_flag()
274
+ elif expected_type == FinalTokenType.OPTION:
275
+ self._settings.logger.debug("Expected `Option` construction.")
276
+ self.validate_token_option()
277
+
278
+ def validate_fallback_defaults(self) -> None:
279
+ """Validates `Fallback` commands with defaults in path if they not written."""
280
+
281
+ result: List[FinalInputProtocol] = []
282
+ commands: List[CommandNode] = self._config.commands if self._config is not None else []
283
+
284
+ found: bool = False
285
+ for part in self._result:
286
+ # Copying.
287
+ found = False
288
+ for node in commands:
289
+ if node.type == "word":
290
+ node = cast(CommandWordNode, node)
291
+
292
+ if not (
293
+ node.name == part.value
294
+ or part.value in [alias.alias_name for alias in node.aliases]
295
+ ):
296
+ continue
297
+ elif node.type == "fallback":
298
+ node = cast(CommandFallbackNode, node)
299
+ else:
300
+ continue
301
+
302
+ result.append(part)
303
+ commands = node.children
304
+ found = True
305
+ break
306
+
307
+ if not found:
308
+ # Invalid path.
309
+ return
310
+
311
+ while commands != []:
312
+ # Searching `Fallback`s.
313
+ found = False
314
+
315
+ for node in commands:
316
+ if node.type == "fallback":
317
+ node = cast(CommandFallbackNode, node)
318
+ result.append(self.cast_token(FinalInputTokenWord(str(node.default))))
319
+ commands = node.children
320
+ found = True
321
+ break
322
+
323
+ if not found:
324
+ # Invalid path.
325
+ return
326
+
327
+ self._result = result
328
+
329
+ def validate_input(self, settings: Settings) -> List[FinalInputProtocol]:
330
+ """Validates a user input.
331
+
332
+ Args:
333
+ settings: The settings.
334
+
335
+ Returns:
336
+ The list with final validated tokens.
337
+ """
338
+
339
+ self._settings.logger.debug(
340
+ f"InputValidator.validate_input(input_tokens='{self._input_tokens}')"
341
+ )
342
+
343
+ self._settings = settings
344
+ self._result = []
345
+ self._i = 0
346
+
347
+ while self._i < len(self._input_tokens):
348
+ self._settings.logger.debug(f"New iteration: {self._i}.")
349
+
350
+ token: RawInputToken = self._input_tokens[self._i]
351
+
352
+ if token.type == RawTokenType.WORD:
353
+ self._settings.logger.debug("Process `Word` token.")
354
+ self.validate_token_word()
355
+
356
+ elif token.type == RawTokenType.MINUS:
357
+ self._settings.logger.debug("Process `Minus` token.")
358
+ self.validate_token_minus()
359
+
360
+ elif token.type == RawTokenType.SPACE:
361
+ self._settings.logger.debug("Process `Space` token.")
362
+
363
+ elif token.type == RawTokenType.STRING:
364
+ self._settings.logger.debug("Process `String` token.")
365
+ self._result.append(self.cast_token(FinalInputTokenWord(token.value)))
366
+
367
+ else:
368
+ self._settings.logger.debug(f"Process unknown token: {token}.")
369
+ self._settings.warning(
370
+ Strings.UNKNOWN_TOKEN,
371
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
372
+ InputStructureWarning,
373
+ InputStructureError
374
+ )
375
+
376
+ self._i += 1
377
+
378
+ self.validate_fallback_defaults()
379
+
380
+ return self._result
381
+
382
+ class ParsedInputValidator:
383
+ """Validator class for parsed input dict."""
384
+
385
+ @staticmethod
386
+ def validate_commands_path(
387
+ settings: Settings,
388
+ input_dict: InputDict,
389
+ config: CommandsConfig
390
+ ) -> bool:
391
+ """Validates a commands path, flags and options.
392
+
393
+ Args:
394
+ settings: The settings.
395
+ input_dict: The parsed input dictionary.
396
+ config: The parsed commands config.
397
+
398
+ Returns:
399
+ `True` if commands path, flags and options is valid, else `False`.
400
+
401
+ Raises:
402
+ InputValuesWarning: If the first command is not written in current state in the settings or path cannot be accessed to the input path.
403
+
404
+ InputValuesError: If the first command is not written in current state in the settings or path cannot be accessed to the input path.
405
+ """
406
+
407
+ flag_keys: List[str] = list(input_dict["flags"].keys())
408
+ option_keys: List[str] = list(input_dict["options"].keys())
409
+ validated_flags: Dict[str, bool] = {n: False for n in flag_keys}
410
+ validated_options: Dict[str, bool] = {n: False for n in option_keys}
411
+
412
+ def validate_flags(children: List[CommandNode]) -> None:
413
+ """Validates the flags.
414
+
415
+ Args:
416
+ children: The commands.
417
+ """
418
+
419
+ for command in children:
420
+ if command.type == "flag":
421
+ command = cast(CommandFlagNode, command)
422
+ validated_flags[command.name] = True
423
+ for alias in command.aliases:
424
+ validated_flags[alias.alias_name] = True
425
+
426
+ def validate_options(children: List[CommandNode]) -> None:
427
+ """Validates the options.
428
+
429
+ Args:
430
+ children: The commands.
431
+ """
432
+
433
+ for command in children:
434
+ if command.type == "option":
435
+ command = cast(CommandOptionNode, command)
436
+ validated_options[command.name] = True
437
+ for alias in command.aliases:
438
+ validated_options[alias.alias_name] = True
439
+
440
+ def validate_command(children: List[CommandNode], i: int) -> bool:
441
+ """Validates a command recursively.
442
+
443
+ Args:
444
+ children: The commands.
445
+ i: Current path index.
446
+
447
+ Returns:
448
+ `False` if command not in current state (i == 0 -> First command) or has not children, else `True`.
449
+
450
+ Raises:
451
+ InputValuesWarning: If the first command is not written in current state in the settings or no commands in children in path.
452
+
453
+ InputValuesError: If the first command is not written in current state in the settings or no commands in children in path.
454
+ """
455
+
456
+ for command in children:
457
+ if command.type not in ("word", "fallback"):
458
+ # Type filtration for positioned commands.
459
+ continue
460
+
461
+ command = cast(Union[CommandWordNode, CommandFallbackNode], command)
462
+
463
+ if command.type == "word":
464
+ # Conditions:
465
+ # 1. `command.type == "word"`` -> Shall equals with original command key or with alias.
466
+ # 2. `command.type == "fallback"` -> None (accepts any word).
467
+ command = cast(CommandWordNode, command)
468
+
469
+ if input_dict["path"][i] == command.name:
470
+ # Continue current iteration.
471
+ pass
472
+ elif input_dict["path"][i] not in [
473
+ alias.alias_name for alias in command.aliases
474
+ ]:
475
+ # Next iteration.
476
+ continue
477
+
478
+ if i == 0:
479
+ # First iteration must has root command.
480
+ in_state: bool = False
481
+ for state_node in config.states:
482
+ if state_node.is_internal:
483
+ if (
484
+ state_node.name == InternalState.BASE.value
485
+ and command.name in state_node.commands
486
+ ):
487
+ # First state in `__base__` state, which defines any current state.
488
+ in_state = True
489
+ break
490
+ if settings.current_state == state_node.name:
491
+ # Command defined in current state.
492
+ in_state = command.name in state_node.commands
493
+ break
494
+
495
+ if not in_state:
496
+ # First iteration not in states.
497
+ settings.warning(
498
+ Strings.COMMAND_NOT_IN_CURRENT_STATE.substitute(
499
+ state=settings.current_state
500
+ ),
501
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
502
+ InputValuesWarning,
503
+ InputValuesError
504
+ )
505
+ return False
506
+
507
+ validate_flags(command.children)
508
+ validate_options(command.children)
509
+
510
+ if i < len(input_dict["path"]) - 1:
511
+ validate_command(command.children, i + 1)
512
+
513
+ return True
514
+
515
+ settings.warning(
516
+ Strings.INPUT_PATH_INVALID.substitute(name=input_dict["path"][i]),
517
+ Strings.AUTO_CORRECT_WITH_SKIPPING,
518
+ InputValuesWarning,
519
+ InputValuesError
520
+ )
521
+
522
+ return False
523
+
524
+ if input_dict["path"] == []:
525
+ # Current path is empty.
526
+ validate_flags(config.commands)
527
+ validate_options(config.commands)
528
+
529
+ return all(validated_flags.values()) and all(validated_options.values())
530
+
531
+ if not validate_command(config.commands, 0):
532
+ return False
533
+
534
+ validate_flags(config.commands)
535
+ validate_options(config.commands)
536
+
537
+ return all(validated_flags.values()) and all(validated_options.values())
538
+
539
+ @staticmethod
540
+ def validate_input_dict(
541
+ settings: Settings,
542
+ input_dict: InputDict,
543
+ config: CommandsConfig
544
+ ) -> bool:
545
+ """Validates input dict with current context in settings.
546
+
547
+ Args:
548
+ settings: The settings.
549
+ input_dict: The input dictionary.
550
+ config: The parsed commands config.
551
+
552
+ Returns:
553
+ `True` if the input is valid, else `False`.
554
+ """
555
+
556
+ return ParsedInputValidator.validate_commands_path(settings, input_dict, config)