feat: impl docs (#4)

The entire public api is covered with documentation in two languages - Russian and English.

the library now supports the latest three versions of python - 3.12, 3.13 and 3.14

minor design changes: now, when a Boolean flag is entered, its value is an empty string, not None.

tests have been adapted to the supported versions of python, readmi has been redesigned in two languages, German is no longer available.
This commit is contained in:
kolo
2025-12-04 21:55:19 +03:00
committed by GitHub
parent a2ac6a608f
commit ce7e24b924
210 changed files with 13770 additions and 1183 deletions
+8 -12
View File
@@ -1,12 +1,8 @@
__all__ = [
"Command",
"PossibleValues",
"PredefinedFlags",
"InputCommand",
"Flags",
"Flag"
]
from argenta.command.models import Command, InputCommand
from argenta.command.flag import defaults as PredefinedFlags
from argenta.command.flag import (Flag, Flags, PossibleValues)
from argenta.command.flag import Flag as Flag
from argenta.command.flag import Flags as Flags
from argenta.command.flag import InputFlag as InputFlag
from argenta.command.flag import InputFlags as InputFlags
from argenta.command.flag import PossibleValues as PossibleValues
from argenta.command.flag.defaults import PredefinedFlags as PredefinedFlags
from argenta.command.models import Command as Command
from argenta.command.models import InputCommand as InputCommand
+13 -5
View File
@@ -1,12 +1,21 @@
from argenta.command.flag.models import Flag, InputFlag
__all__ = [
"InputCommandException",
"UnprocessedInputFlagException",
"RepeatedInputFlagsException",
"EmptyInputCommandException",
]
from abc import ABC, abstractmethod
from typing import override
from argenta.command.flag.models import Flag, InputFlag
class InputCommandException(ABC, Exception):
"""
Private. Base exception class for all exceptions raised when parse input command
"""
@override
@abstractmethod
def __str__(self) -> str:
@@ -17,6 +26,7 @@ class UnprocessedInputFlagException(InputCommandException):
"""
Private. Raised when an unprocessed input flag is detected
"""
@override
def __str__(self) -> str:
return "Unprocessed Input Flags"
@@ -34,16 +44,14 @@ class RepeatedInputFlagsException(InputCommandException):
@override
def __str__(self) -> str:
string_entity: str = self.flag.string_entity
return (
"Repeated Input Flags\n"
f"Duplicate flag was detected in the input: '{string_entity}'"
)
return f"Repeated Input Flags\nDuplicate flag was detected in the input: '{string_entity}'"
class EmptyInputCommandException(InputCommandException):
"""
Private. Raised when an empty input command is detected
"""
@override
def __str__(self) -> str:
return "Input Command is empty"
+6 -11
View File
@@ -1,11 +1,6 @@
__all__ = [
"Flag",
"InputFlag",
"Flags",
"PossibleValues",
"ValidationStatus"
]
from argenta.command.flag.models import Flag, InputFlag, PossibleValues, ValidationStatus
from argenta.command.flag.flags.models import Flags
from argenta.command.flag.flags.models import Flags as Flags
from argenta.command.flag.flags.models import InputFlags as InputFlags
from argenta.command.flag.models import Flag as Flag
from argenta.command.flag.models import InputFlag as InputFlag
from argenta.command.flag.models import PossibleValues as PossibleValues
from argenta.command.flag.models import ValidationStatus as ValidationStatus
+21 -19
View File
@@ -1,27 +1,29 @@
from typing import Literal
from argenta.command.flag.models import Flag, PossibleValues
import re
__all__ = ["PredefinedFlags"]
import re
from typing import Literal
from argenta.command.flag.models import Flag, PossibleValues
DEFAULT_PREFIX: Literal["-", "--", "---"] = "-"
HELP = Flag(name="help", possible_values=PossibleValues.NEITHER)
SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
INFO = Flag(name="info", possible_values=PossibleValues.NEITHER) # noqa: WPS110
SHORT_INFO = Flag(name="I", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
class PredefinedFlags:
HELP = Flag(name="help", possible_values=PossibleValues.NEITHER)
SHORT_HELP = Flag(name="H", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
ALL = Flag(name="all", possible_values=PossibleValues.NEITHER)
SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
INFO = Flag(name="info", possible_values=PossibleValues.NEITHER) # noqa: WPS110
SHORT_INFO = Flag(name="I", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
HOST = Flag(
name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
)
SHORT_HOST = Flag(
name="H",
prefix=DEFAULT_PREFIX,
possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"),
)
ALL = Flag(name="all", possible_values=PossibleValues.NEITHER)
SHORT_ALL = Flag(name="A", prefix=DEFAULT_PREFIX, possible_values=PossibleValues.NEITHER)
PORT = Flag(name="port", possible_values=re.compile(r"^\d{1,5}$"))
SHORT_PORT = Flag(name="P", prefix=DEFAULT_PREFIX, possible_values=re.compile(r"^\d{1,5}$"))
HOST = Flag(name="host", possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"))
SHORT_HOST = Flag(
name="H",
prefix=DEFAULT_PREFIX,
possible_values=re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$"),
)
PORT = Flag(name="port", possible_values=re.compile(r"^\d{1,5}$"))
SHORT_PORT = Flag(name="P", prefix=DEFAULT_PREFIX, possible_values=re.compile(r"^\d{1,5}$"))
+2 -10
View File
@@ -1,10 +1,2 @@
__all__ = [
"Flags",
"InputFlags"
]
from argenta.command.flag.flags.models import (
Flags,
InputFlags
)
from argenta.command.flag.flags.models import Flags as Flags
from argenta.command.flag.flags.models import InputFlags as InputFlags
+11 -7
View File
@@ -1,7 +1,9 @@
from argenta.command.flag.models import InputFlag, Flag
from typing import Generic, TypeVar, override
from collections.abc import Iterator
__all__ = ["Flags", "InputFlags"]
from collections.abc import Iterator
from typing import Generic, TypeVar, override
from argenta.command.flag.models import Flag, InputFlag
FlagType = TypeVar("FlagType")
@@ -30,6 +32,9 @@ class BaseFlags(Generic[FlagType]):
:return: None
"""
self.flags.extend(flags)
def __len__(self) -> int:
return len(self.flags)
def __iter__(self) -> Iterator[FlagType]:
return iter(self.flags)
@@ -52,7 +57,7 @@ class Flags(BaseFlags[Flag]):
:return: entity of the flag or None
"""
return next((flag for flag in self.flags if flag.name == name), None)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, Flags):
@@ -82,9 +87,9 @@ class InputFlags(BaseFlags[InputFlag]):
:return: entity of the flag or None
"""
return next((flag for flag in self.flags if flag.name == name), None)
@override
def __eq__(self, other: object) -> bool:
def __eq__(self, other: object) -> bool:
if not isinstance(other, InputFlags):
raise NotImplementedError
@@ -103,4 +108,3 @@ class InputFlags(BaseFlags[InputFlag]):
return False
else:
raise TypeError
+44 -34
View File
@@ -1,23 +1,30 @@
__all__ = ["PossibleValues", "ValidationStatus", "Flag", "InputFlag"]
from enum import Enum
from re import Pattern
from typing import Literal, override
PREFIX_TYPE = Literal["-", "--", "---"]
class PossibleValues(Enum):
NEITHER = 'NEITHER'
ALL = 'ALL'
NEITHER = "NEITHER"
ALL = "ALL"
class ValidationStatus(Enum):
VALID = 'VALID'
INVALID = 'INVALID'
UNDEFINED = 'UNDEFINED'
VALID = "VALID"
INVALID = "INVALID"
UNDEFINED = "UNDEFINED"
class Flag:
def __init__(
self, name: str, *,
prefix: Literal["-", "--", "---"] = "--",
self,
name: str,
*,
prefix: PREFIX_TYPE = "--",
possible_values: list[str] | Pattern[str] | PossibleValues = PossibleValues.ALL,
) -> None:
"""
@@ -28,26 +35,29 @@ class Flag:
:return: None
"""
self.name: str = name
self.prefix: Literal["-", "--", "---"] = prefix
self.prefix: PREFIX_TYPE = prefix
self.possible_values: list[str] | Pattern[str] | PossibleValues = possible_values
def validate_input_flag_value(self, input_flag_value: str | None) -> bool:
def validate_input_flag_value(self, input_flag_value: str) -> bool:
"""
Private. Validates the input flag value
:param input_flag_value: The input flag value to validate
:return: whether the entered flag is valid as bool
"""
if self.possible_values == PossibleValues.NEITHER:
return input_flag_value is None
return input_flag_value == ''
if self.possible_values == PossibleValues.ALL:
return input_flag_value != ''
if isinstance(self.possible_values, Pattern):
return isinstance(input_flag_value, str) and bool(self.possible_values.match(input_flag_value))
return bool(self.possible_values.match(input_flag_value))
if isinstance(self.possible_values, list):
return input_flag_value in self.possible_values
return False
return True
@property
def string_entity(self) -> str:
"""
@@ -56,17 +66,17 @@ class Flag:
"""
string_entity: str = self.prefix + self.name
return string_entity
@override
def __str__(self) -> str:
return self.string_entity
@override
def __repr__(self) -> str:
return f'Flag<name={self.name}, prefix={self.prefix}>'
return f"Flag<name={self.name}, prefix={self.prefix}>"
@override
def __eq__(self, other: object) -> bool:
def __eq__(self, other: object) -> bool:
if isinstance(other, Flag):
return self.string_entity == other.string_entity
else:
@@ -75,10 +85,12 @@ class Flag:
class InputFlag:
def __init__(
self, name: str, *,
prefix: Literal['-', '--', '---'] = '--',
input_value: str | None,
status: ValidationStatus | None
self,
name: str,
*,
prefix: PREFIX_TYPE = "--",
input_value: str,
status: ValidationStatus | None,
):
"""
Public. The entity of the flag of the entered command
@@ -88,10 +100,10 @@ class InputFlag:
:return: None
"""
self.name: str = name
self.prefix: Literal['-', '--', '---'] = prefix
self.input_value: str | None = input_value
self.prefix: PREFIX_TYPE = prefix
self.input_value: str = input_value
self.status: ValidationStatus | None = status
@property
def string_entity(self) -> str:
"""
@@ -103,17 +115,15 @@ class InputFlag:
@override
def __str__(self) -> str:
return f'{self.string_entity} {self.input_value}'
@override
def __repr__(self) -> str:
return f'InputFlag<name={self.name}, prefix={self.prefix}, value={self.input_value}, status={self.status}>'
return f"{self.string_entity} {self.input_value}"
@override
def __eq__(self, other: object) -> bool:
def __repr__(self) -> str:
return f"InputFlag<name={self.name}, prefix={self.prefix}, value={self.input_value}, status={self.status}>"
@override
def __eq__(self, other: object) -> bool:
if isinstance(other, InputFlag):
return (
self.name == other.name
)
return self.name == other.name
else:
raise NotImplementedError
+63 -85
View File
@@ -1,18 +1,23 @@
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
from argenta.command.flag.flags.models import InputFlags, Flags
from argenta.command.exceptions import (
UnprocessedInputFlagException,
RepeatedInputFlagsException,
EmptyInputCommandException,
)
__all__ = ["Command", "InputCommand"]
import shlex
from typing import Never, Self, cast, Literal
from argenta.command.exceptions import (
EmptyInputCommandException,
RepeatedInputFlagsException,
UnprocessedInputFlagException,
)
from argenta.command.flag.flags.models import Flags, InputFlags
from argenta.command.flag.models import Flag, InputFlag, ValidationStatus
ParseFlagsResult = tuple[InputFlags, str | None, str | None]
ParseResult = tuple[str, InputFlags]
MIN_FLAG_PREFIX: str = "-"
PREFIX_TYPE = Literal["-", "--", "---"]
DEFAULT_WITHOUT_FLAGS: Flags = Flags()
DEFAULT_WITHOUT_ALIASES: list[Never] = []
DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
@@ -20,10 +25,11 @@ DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags()
class Command:
def __init__(
self,
trigger: str, *,
description: str | None = None,
trigger: str,
*,
description: str = "Some useful command",
flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS,
aliases: list[str] | None = None,
aliases: list[str] | list[Never] = DEFAULT_WITHOUT_ALIASES,
):
"""
Public. The command that can and should be registered in the Router
@@ -34,12 +40,10 @@ class Command:
"""
self.registered_flags: Flags = flags if isinstance(flags, Flags) else Flags([flags])
self.trigger: str = trigger
self.description: str = description if description else "Command without description"
self.aliases: list[str] = aliases if aliases else []
self.description: str = description
self.aliases: list[str] | list[Never] = aliases
def validate_input_flag(
self, flag: InputFlag
) -> ValidationStatus:
def validate_input_flag(self, flag: InputFlag) -> ValidationStatus:
"""
Private. Validates the input flag
:param flag: input flag for validation
@@ -57,8 +61,7 @@ class Command:
class InputCommand:
def __init__(self, trigger: str, *,
input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
def __init__(self, trigger: str, *, input_flags: InputFlag | InputFlags = DEFAULT_WITHOUT_INPUT_FLAGS):
"""
Private. The model of the input command, after parsing
:param trigger:the trigger of the command
@@ -66,7 +69,9 @@ class InputCommand:
:return: None
"""
self.trigger: str = trigger
self.input_flags: InputFlags = input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags])
self.input_flags: InputFlags = (
input_flags if isinstance(input_flags, InputFlags) else InputFlags([input_flags])
)
@classmethod
def parse(cls, raw_command: str) -> Self:
@@ -75,77 +80,50 @@ class InputCommand:
:param raw_command: raw input command
:return: model of the input command, after parsing as InputCommand
"""
trigger, input_flags = CommandParser(raw_command).parse_raw_command()
return cls(trigger=trigger, input_flags=input_flags)
tokens = shlex.split(raw_command)
class CommandParser:
def __init__(self, raw_command: str) -> None:
self.raw_command: str = raw_command
self._parsed_input_flags: InputFlags = InputFlags()
def parse_raw_command(self) -> ParseResult:
if not self.raw_command:
raise EmptyInputCommandException()
input_flags, crnt_flag_name, crnt_flag_val = self._parse_flags(self.raw_command.split()[1:])
if any([crnt_flag_name, crnt_flag_val]):
raise UnprocessedInputFlagException()
else:
return (self.raw_command.split()[0], input_flags)
def _parse_flags(self, _tokens: list[str] | list[Never]) -> ParseFlagsResult:
crnt_flg_name, crnt_flg_val = None, None
for index, token in enumerate(_tokens):
crnt_flg_name, crnt_flg_val = _parse_single_token(token, crnt_flg_name, crnt_flg_val)
if not crnt_flg_name or self._is_next_token_value(index, _tokens):
continue
if not tokens:
raise EmptyInputCommandException
command = tokens[0]
flags: InputFlags = InputFlags()
i = 1
while i < len(tokens):
token = tokens[i]
if token.startswith("---"):
prefix = "---"
name = token[3:]
elif token.startswith("--"):
prefix = "--"
name = token[2:]
elif token.startswith("-"):
prefix = "-"
name = token[1:]
else:
raise UnprocessedInputFlagException
if not name:
raise UnprocessedInputFlagException
if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"):
input_value = tokens[i + 1]
i += 2
else:
input_value = ""
i += 1
input_flag = InputFlag(
name=crnt_flg_name[crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1:],
prefix=cast(
Literal["-", "--", "---"],
crnt_flg_name[:crnt_flg_name.rfind(MIN_FLAG_PREFIX) + 1],
),
input_value=crnt_flg_val,
name=name,
prefix=cast(PREFIX_TYPE, prefix), # pyright: ignore[reportUnnecessaryCast]
input_value=input_value,
status=None
)
if input_flag in self._parsed_input_flags:
if input_flag in flags:
raise RepeatedInputFlagsException(input_flag)
self._parsed_input_flags.add_flag(input_flag)
crnt_flg_name, crnt_flg_val = None, None
return (self._parsed_input_flags, crnt_flg_name, crnt_flg_val)
def _is_next_token_value(self, current_index: int,
_tokens: list[str] | list[Never]) -> bool:
next_index = current_index + 1
if next_index >= len(_tokens):
return False
flags.add_flag(input_flag)
next_token = _tokens[next_index]
return not next_token.startswith(MIN_FLAG_PREFIX)
def _parse_single_token(
token: str,
crnt_flag_name: str | None,
crnt_flag_val: str | None
) -> tuple[str | None, str | None]:
if not token.startswith(MIN_FLAG_PREFIX):
if not crnt_flag_name or crnt_flag_val:
raise UnprocessedInputFlagException
return crnt_flag_name, token
prefix = token[:token.rfind(MIN_FLAG_PREFIX)]
if len(token) < 2 or len(prefix) > 2:
raise UnprocessedInputFlagException
new_flag_name = token
new_flag_value = None
return new_flag_name, new_flag_value
return cls(command, input_flags=flags)