From 6b202c256c8393ea41c6a5098d2955394b933a30 Mon Sep 17 00:00:00 2001 From: DogiFnf Date: Tue, 7 Jan 2025 14:24:55 +0300 Subject: [PATCH 1/5] modified: PyroArgs/errors/CommandError.py modified: PyroArgs/parser.py --- PyroArgs/errors/CommandError.py | 6 +++--- PyroArgs/parser.py | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/PyroArgs/errors/CommandError.py b/PyroArgs/errors/CommandError.py index 3cd06d9..ebeff10 100644 --- a/PyroArgs/errors/CommandError.py +++ b/PyroArgs/errors/CommandError.py @@ -6,16 +6,16 @@ class CommandError(Exception): def __init__( self, - name: str, + command: str, message: Message, parsed_args: List[Any], parsed_kwargs: Dict[str, Any], error_message: str = None, original_error: Exception = None ): - full_message = f'Command error: Error in command "{name}".' + full_message = f'Command error: Error in command "{command}".' super().__init__(full_message) - self.name = name + self.command = command self.message = message self.parsed_args = parsed_args self.parsed_kwargs = parsed_kwargs diff --git a/PyroArgs/parser.py b/PyroArgs/parser.py index c68e19d..c6ad9a0 100644 --- a/PyroArgs/parser.py +++ b/PyroArgs/parser.py @@ -47,8 +47,8 @@ def get_command_and_args(text: str, prefixes: Union[List[str], Tuple[str], str]) def parse_command( - func: Callable, command: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no') + func: Callable, command: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), + falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') ) -> Any: """ Executes the given function `func` with arguments parsed from the `command` string. @@ -65,8 +65,8 @@ def parse_command( Raises: ValueError: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. """ - def get_bool(arg: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no')) -> bool: + def get_bool(arg: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), + falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f')) -> bool: """ Converts a string argument to a boolean. @@ -95,9 +95,10 @@ def get_bool(arg: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes' signature: inspect.Signature = inspect.signature(func) lexer: shlex.shlex = shlex.shlex(command.strip(), posix=True) lexer.whitespace_split = True - lexer.escapedquotes = '"' - lexer.quotes = '"' + lexer.escapedquotes = '' + lexer.quotes = '' lexer.whitespace = ' ' + lexer.commenters = '' args: List[str] = list(lexer) args_counter: int = 0 From fcf6e4c896e98109ca463a1e00b788491ff0866d Mon Sep 17 00:00:00 2001 From: DogiFnf Date: Sat, 5 Apr 2025 10:55:55 +0300 Subject: [PATCH 2/5] modified: PyroArgs/parser.py --- PyroArgs/parser.py | 78 +++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/PyroArgs/parser.py b/PyroArgs/parser.py index c6ad9a0..452fdaf 100644 --- a/PyroArgs/parser.py +++ b/PyroArgs/parser.py @@ -19,17 +19,14 @@ def get_command_and_args(text: str, prefixes: Union[List[str], Tuple[str], str]) Finally, it takes the rest of the string as the arguments, strips it of any whitespaces and returns a tuple with the command and arguments. - Parameters - ---------- - text : str - The string to parse. - prefixes : List[str] or Tuple[str] or str - The list/tuple or single prefix to check against. - - Returns - ------- - Tuple[str, str] - The command and arguments as a tuple. + ## Parameters + text (``str``): + The string to parse. + prefixes : (``List[str]`` or ``Tuple[str]`` or ``str``): + The ``list/tuple`` or single prefix to check against. + + ## Returns + ``Tuple[str, str]``: The command and arguments as a tuple. """ text = text.strip() @@ -47,39 +44,56 @@ def get_command_and_args(text: str, prefixes: Union[List[str], Tuple[str], str]) def parse_command( - func: Callable, command: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), + func: Callable, + command: str, + trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') ) -> Any: """ Executes the given function `func` with arguments parsed from the `command` string. - Args: - func (Callable): The function to be executed. - command (str): The command string containing arguments for the function. - trues (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as True. - falses (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as False. + ## Parameters + func (``Callable``): + The function to be executed. - Returns: - Any: The result of executing `func` with the parsed arguments. + command (``str``): + The command string containing arguments for the function. - Raises: - ValueError: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. + trues (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as True. + + falses (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as False. + + ## Returns + ``Any``: The result of executing `func` with the parsed arguments. + + ## Raises + ``ValueError``: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. """ - def get_bool(arg: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f')) -> bool: + def get_bool( + arg: str, + trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), + falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') + ) -> bool: """ Converts a string argument to a boolean. - Args: - arg (str): The argument to be converted. - trues (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as True. - falses (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as False. + ## Parameters + arg (``str``): + The argument to be converted. + + trues (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as True. + + falses (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as False. - Returns: - bool: The converted boolean value. + ## Returns + ``bool``: The converted boolean value. - Raises: - ValueError: If the argument is not in the trues or falses lists. + ## Raises + ``ValueError``: If the argument is not in the trues or falses lists. """ if isinstance(trues, str): trues = [trues] @@ -96,7 +110,7 @@ def get_bool(arg: str, trues: Union[List[str], Tuple[str], str] = ('true', 'yes' lexer: shlex.shlex = shlex.shlex(command.strip(), posix=True) lexer.whitespace_split = True lexer.escapedquotes = '' - lexer.quotes = '' + lexer.quotes = ' ' lexer.whitespace = ' ' lexer.commenters = '' args: List[str] = list(lexer) From efe0514135792ac53f2ca4770d7c5595620622b5 Mon Sep 17 00:00:00 2001 From: DogiFnf Date: Sun, 6 Apr 2025 02:26:49 +0300 Subject: [PATCH 3/5] modified: PyroArgs/errors/CommandPermissionError.py modified: PyroArgs/errors/MissingArgumentError.py modified: PyroArgs/errors/__init__.py deleted: PyroArgs/parser.py modified: PyroArgs/pyroargs.py modified: PyroArgs/types/commandRegistry.py modified: PyroArgs/types/events.py modified: PyroArgs/types/logger.py --- PyroArgs/errors/CommandPermissionError.py | 3 +- PyroArgs/errors/MissingArgumentError.py | 3 +- PyroArgs/errors/__init__.py | 3 +- PyroArgs/parser.py | 225 ---------------------- PyroArgs/pyroargs.py | 83 ++++++-- PyroArgs/types/commandRegistry.py | 2 +- PyroArgs/types/events.py | 67 +++++-- PyroArgs/types/logger.py | 57 ++++-- 8 files changed, 169 insertions(+), 274 deletions(-) delete mode 100644 PyroArgs/parser.py diff --git a/PyroArgs/errors/CommandPermissionError.py b/PyroArgs/errors/CommandPermissionError.py index cfe2079..d41ba92 100644 --- a/PyroArgs/errors/CommandPermissionError.py +++ b/PyroArgs/errors/CommandPermissionError.py @@ -9,7 +9,8 @@ def __init__( message: Message, permission_level: int ): - full_message = f'Permissions error: User does not have permission to use command "{name}".' + full_message = ('Permissions error: User does not ' + f'have permission to use command "{name}".') super().__init__(full_message) self.name = name self.message = message diff --git a/PyroArgs/errors/MissingArgumentError.py b/PyroArgs/errors/MissingArgumentError.py index 0e2ebf3..36b61df 100644 --- a/PyroArgs/errors/MissingArgumentError.py +++ b/PyroArgs/errors/MissingArgumentError.py @@ -15,7 +15,8 @@ def __init__( missing_arg_position: int ): full_message = ( - f'MissingArgumentError: Missing required argument "{missing_arg_name}" at position {missing_arg_position}.' + ('MissingArgumentError: Missing required argument' + f' "{missing_arg_name}" at position {missing_arg_position}.') ) super().__init__(name, message_object, parsed_args, diff --git a/PyroArgs/errors/__init__.py b/PyroArgs/errors/__init__.py index 88df8f8..f11effd 100644 --- a/PyroArgs/errors/__init__.py +++ b/PyroArgs/errors/__init__.py @@ -6,4 +6,5 @@ from .ArgumentTypeError import ArgumentTypeError __all__ = ['ArgumentsError', 'CommandError', - 'CommandPermissionError', 'MissingArgumentError', 'ArgumentTypeError'] + 'CommandPermissionError', 'MissingArgumentError', + 'ArgumentTypeError'] diff --git a/PyroArgs/parser.py b/PyroArgs/parser.py deleted file mode 100644 index 452fdaf..0000000 --- a/PyroArgs/parser.py +++ /dev/null @@ -1,225 +0,0 @@ -from typing import Dict, Any, List, Callable, Union, Tuple -from . import errors -import inspect -import shlex - - -def get_command_and_args(text: str, prefixes: Union[List[str], Tuple[str], str]) -> Tuple[str, str]: - """ - Gets command and arguments from a string. - - The function takes a string and a list/tuple of prefixes as parameters. - It first strips the string of any whitespaces, then checks if the string - starts with any of the prefixes. If not, it raises a NameError. - - Then it splits the string by spaces and takes the first part as the command. - It goes through the list of prefixes and checks if the command starts with - any of them. If it does, it removes the prefix from the command. - - Finally, it takes the rest of the string as the arguments, strips it of any - whitespaces and returns a tuple with the command and arguments. - - ## Parameters - text (``str``): - The string to parse. - prefixes : (``List[str]`` or ``Tuple[str]`` or ``str``): - The ``list/tuple`` or single prefix to check against. - - ## Returns - ``Tuple[str, str]``: The command and arguments as a tuple. - """ - text = text.strip() - - if not text.startswith(tuple(prefixes)): - raise NameError('Command does not start with prefix.') - - cmd = text.split()[0] - for prefix in prefixes: - if cmd.startswith(prefix): - cmd = cmd[len(prefix):] - break - - args = text[len(prefix)+len(cmd):].strip() - return cmd, args - - -def parse_command( - func: Callable, - command: str, - trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') -) -> Any: - """ - Executes the given function `func` with arguments parsed from the `command` string. - - ## Parameters - func (``Callable``): - The function to be executed. - - command (``str``): - The command string containing arguments for the function. - - trues (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as True. - - falses (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as False. - - ## Returns - ``Any``: The result of executing `func` with the parsed arguments. - - ## Raises - ``ValueError``: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. - """ - def get_bool( - arg: str, - trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') - ) -> bool: - """ - Converts a string argument to a boolean. - - ## Parameters - arg (``str``): - The argument to be converted. - - trues (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as True. - - falses (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as False. - - ## Returns - ``bool``: The converted boolean value. - - ## Raises - ``ValueError``: If the argument is not in the trues or falses lists. - """ - if isinstance(trues, str): - trues = [trues] - if isinstance(falses, str): - falses = [falses] - if arg.lower() in trues: - return True - if arg.lower() in falses: - return False - raise ValueError( - f'Failed to cast argument "{arg}" to bool.') - - signature: inspect.Signature = inspect.signature(func) - lexer: shlex.shlex = shlex.shlex(command.strip(), posix=True) - lexer.whitespace_split = True - lexer.escapedquotes = '' - lexer.quotes = ' ' - lexer.whitespace = ' ' - lexer.commenters = '' - args: List[str] = list(lexer) - - args_counter: int = 0 - result_args: List[Any] = [] - result_kwargs: Dict[str, Any] = {} - is_keyword_only_used: bool = False - - for param in signature.parameters.values(): - if param.kind == param.VAR_POSITIONAL: - raise SyntaxError( - f'Positional var are not supported. Remove "*{param.name}".' - ) - elif param.kind == param.VAR_KEYWORD: - raise SyntaxError( - f'Keyword var are not supported. Remove "**{param.name}".' - ) - - for name, param in list(signature.parameters.items())[1:]: - # HINT: print(name, param.kind, param.default, param.annotation) - if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): - default_used: bool = False - try: - arg: str = args[args_counter] - except IndexError: - if param.default != param.empty: - default_used = True - arg = param.default - result_args.append(arg) - else: - raise errors.MissingArgumentError( - name=name, - message_object=None, - missing_arg_name=name, - missing_arg_position=args_counter+1, - parsed_args=args, - parsed_kwargs=result_kwargs - ) - - if not default_used: - if param.annotation == bool: - arg = get_bool(arg, trues, falses) - - elif param.annotation != inspect._empty: - try: - if param.annotation != Any: - arg = param.annotation(arg) - except ValueError: - raise errors.ArgumentTypeError( - name=name, - message_object=None, - parsed_args=args, - parsed_kwargs=result_kwargs, - errored_arg_name=name, - errored_arg_position=args_counter+1, - required_type=param.annotation - ) - result_args.append(arg) - - elif param.kind == param.KEYWORD_ONLY: - if is_keyword_only_used: - raise SyntaxError( - 'There should not be more than one keyword argument in the function call.' - ) - is_keyword_only_used = True - - arg: str = ' '.join(args[args_counter:]) - if not arg: - if param.default != param.empty: - arg = param.default - - if param.annotation == bool: - arg = get_bool(arg, trues, falses) - - elif param.annotation != inspect._empty: - try: - if param.annotation != Any: - if arg == '': - arg = param.annotation() - else: - arg = param.annotation(arg) - except ValueError: - raise errors.ArgumentTypeError( - name=name, - message_object=None, - parsed_args=args, - parsed_kwargs=result_kwargs, - errored_arg_name=name, - errored_arg_position=args_counter+1, - required_type=param.annotation - ) - result_kwargs[name] = arg - - args_counter += 1 - - # return func(*result_args, **result_kwargs) - return result_args, result_kwargs - - -if __name__ == '__main__': - def func(a: str, b: bool = '52') -> None: - print(a, b, sep='\n') - print(type(a), type(b), sep='\n') - return 'RESULT_AR' - - text = '/test 111 true' - cmd, args = get_command_and_args(text, ['/', 'v?']) - - if cmd == 'test': - result_args, result_kwargs = parse_command(func, args) - print(func(*result_args, **result_kwargs)) diff --git a/PyroArgs/pyroargs.py b/PyroArgs/pyroargs.py index b170800..3077693 100644 --- a/PyroArgs/pyroargs.py +++ b/PyroArgs/pyroargs.py @@ -17,7 +17,12 @@ class PyroArgs: - def __init__(self, bot: Client, prefixes: Union[List[str], Tuple[str], str] = ['/'], log_file: str = None) -> None: + def __init__( + self, + bot: Client, + prefixes: Union[List[str], Tuple[str], str] = ['/'], + log_file: str = None + ) -> None: # Переменные класса self.bot: Client = bot self.prefixes: Union[List[str], Tuple[str], str] = prefixes @@ -32,12 +37,12 @@ def __init__(self, bot: Client, prefixes: Union[List[str], Tuple[str], str] = [' # Логи self.setup_logs = self.events.logger.setup_logs - self.before_use_command_message = self.events.logger.before_use_command_message - self.after_use_command_message = self.events.logger.after_use_command_message - self.missing_argument_error_message = self.events.logger.missing_argument_error_message - self.argument_type_error_message = self.events.logger.argument_type_error_message - self.command_error_message = self.events.logger.command_error_message - self.permissions_error_message = self.events.logger.permissions_error_message + self.before_use_command_message = self.events.logger.before_use_command_message # noqa + self.after_use_command_message = self.events.logger.after_use_command_message # noqa + self.missing_argument_error_message = self.events.logger.missing_argument_error_message # noqa + self.argument_type_error_message = self.events.logger.argument_type_error_message # noqa + self.command_error_message = self.events.logger.command_error_message # noqa + self.permissions_error_message = self.events.logger.permissions_error_message # noqa def command( self, @@ -67,16 +72,26 @@ async def handler(client: Client, message: Message) -> None: cmd, args = get_command_and_args(cmd_text, self.prefixes) # ** Проверка прав ** - if not await self.__has_permission(command_name, message, permissions_level): + if not await self.__has_permission( + command_name, + message, + permissions_level + ): return # ** Исключения ** parsed_args = await self.__parse_arguments(func, args, message) if not parsed_args: - return # Ошибка уже обрабатывается внутри __parse_arguments + return # noqa Ошибка уже обрабатывается внутри __parse_arguments # ** Выполнение команды ** - await self.__execute_command(func, message, command_name, args, parsed_args) + await self.__execute_command( + func, + message, + command_name, + args, + parsed_args + ) # ** Регистрация команды ** self.__register_command( @@ -97,8 +112,13 @@ async def handler(client: Client, message: Message) -> None: return decorator - async def __has_permission(self, command_name: str, message: Message, permissions_level: int) -> bool: - if self.permission_checker_func and not await self.permission_checker_func(message, permissions_level): + async def __has_permission( + self, + command_name: str, + message: Message, + permissions_level: int + ) -> bool: + if self.permission_checker_func and not await self.permission_checker_func(message, permissions_level): # noqa error = errors.CommandPermissionError( command=command_name, message=message, @@ -108,7 +128,12 @@ async def __has_permission(self, command_name: str, message: Message, permission return False return True - async def __parse_arguments(self, func: Callable, args: str, message: Message) -> Optional[Tuple[List, Dict]]: + async def __parse_arguments( + self, + func: Callable, + args: str, + message: Message + ) -> Optional[Tuple[List, Dict]]: try: result_args, result_kwargs = parse_command(func, args) return result_args, result_kwargs @@ -129,8 +154,14 @@ async def __parse_arguments(self, func: Callable, args: str, message: Message) - raise SystemError(e) return None - async def __execute_command(self, func: Callable, message: Message, - command_name: str, args: str, parsed_args: Tuple[List, Dict]) -> None: + async def __execute_command( + self, + func: Callable, + message: Message, + command_name: str, + args: str, + parsed_args: Tuple[List, Dict] + ) -> None: result_args, result_kwargs = parsed_args try: await self.events._trigger_before_use_command( @@ -159,8 +190,21 @@ async def __execute_command(self, func: Callable, message: Message, await self.events._trigger_command_error(message, error) raise e - def __register_command(self, handler, all_names, filters, group, command_name, description, - usage, example, permissions_level, aliases, command_meta_data, category): + def __register_command( + self, + handler, + all_names, + filters, + group, + command_name, + description, + usage, + example, + permissions_level, + aliases, + command_meta_data, + category + ): cmd = Command( command_name, description, @@ -179,7 +223,10 @@ def __register_command(self, handler, all_names, filters, group, command_name, d group ) - def permissions_checker(self, func: Callable[[Message, int], bool]) -> Callable[[Message, int], bool]: + def permissions_checker( + self, + func: Callable[[Message, int], bool] + ) -> Callable[[Message, int], bool]: self.permission_checker_func = func return func diff --git a/PyroArgs/types/commandRegistry.py b/PyroArgs/types/commandRegistry.py index c63fc83..957f0f5 100644 --- a/PyroArgs/types/commandRegistry.py +++ b/PyroArgs/types/commandRegistry.py @@ -36,7 +36,7 @@ def iterate_commands(self) -> Iterator[Command]: for cmd in cmds: yield cmd - def iterate_categories_with_commands(self) -> Iterator[Tuple[str, List[Command]]]: + def iterate_categories_with_commands(self) -> Iterator[Tuple[str, List[Command]]]: # noqa """Итерация по всем категориям и командам.""" for category in self.commands: yield category, self.commands[category] diff --git a/PyroArgs/types/events.py b/PyroArgs/types/events.py index c77a3eb..0e5dcc9 100644 --- a/PyroArgs/types/events.py +++ b/PyroArgs/types/events.py @@ -24,18 +24,17 @@ def __init__(self, log_file: str = None) -> None: self._on_missing_argument_error_handlers: List[ErrorHandler] = [] self._on_argument_type_error_handlers: List[ErrorHandler] = [] self._on_command_error_handlers: List[CommandErrorHandler] = [] - self._on_command_permission_error_handlers: List[PermissionErrorHandler] = [ - ] + self._on_command_permission_error_handlers: List[PermissionErrorHandler] = [] # noqa self.logger: Logger = Logger(log_file) # region Декораторы def on_before_use_command(self, func: CommandHandler) -> CommandHandler: - """Декоратор для регистрации обработчиков успешного использования команды.""" + """Декоратор для регистрации обработчиков успешного использования команды.""" # noqa self._on_before_use_command_handlers.append(func) return func def on_after_use_command(self, func: CommandHandler) -> CommandHandler: - """Декоратор для регистрации обработчиков успешного использования команды.""" + """Декоратор для регистрации обработчиков успешного использования команды.""" # noqa self._on_after_use_command_handlers.append(func) return func @@ -49,12 +48,18 @@ def on_argument_type_error(self, func: ErrorHandler) -> ErrorHandler: self._on_argument_type_error_handlers.append(func) return func - def on_command_error(self, func: CommandErrorHandler) -> CommandErrorHandler: + def on_command_error( + self, + func: CommandErrorHandler + ) -> CommandErrorHandler: """Декоратор для регистрации обработчиков ошибок команд.""" self._on_command_error_handlers.append(func) return func - def on_command_permission_error(self, func: PermissionErrorHandler) -> PermissionErrorHandler: + def on_command_permission_error( + self, + func: PermissionErrorHandler + ) -> PermissionErrorHandler: """Декоратор для регистрации обработчиков ошибок команд.""" self._on_command_permission_error_handlers.append(func) return func @@ -62,39 +67,73 @@ def on_command_permission_error(self, func: PermissionErrorHandler) -> Permissio # region Триггеры async def _trigger_before_use_command( - self, message: Message, command: str, args: List[Any], kwargs: Dict[str, Any] + self, + message: Message, + command: str, + args: List[Any], + kwargs: Dict[str, Any] ) -> None: - await self.logger._trigger_before_use_command(message, command, args, kwargs) + await self.logger._trigger_before_use_command( + message, + command, + args, + kwargs + ) for handler in self._on_before_use_command_handlers: await handler(message, command, args, kwargs) async def _trigger_after_use_command( - self, message: Message, command: str, args: List[Any], kwargs: Dict[str, Any] + self, + message: Message, + command: str, + args: List[Any], + kwargs: Dict[str, Any] ) -> None: - await self.logger._trigger_after_use_command(message, command, args, kwargs) + await self.logger._trigger_after_use_command( + message, + command, + args, + kwargs + ) for handler in self._on_after_use_command_handlers: await handler(message, command, args, kwargs) - async def _trigger_missing_argument_error(self, message: Message, error: errors.MissingArgumentError) -> None: + async def _trigger_missing_argument_error( + self, + message: Message, + error: errors.MissingArgumentError + ) -> None: await self.logger._trigger_missing_argument_error(message, error) if not self._on_missing_argument_error_handlers: raise error for handler in self._on_missing_argument_error_handlers: await handler(message, error) - async def _trigger_argument_type_error(self, message: Message, error: errors.ArgumentTypeError) -> None: + async def _trigger_argument_type_error( + self, + message: Message, + error: errors.ArgumentTypeError + ) -> None: await self.logger._trigger_argument_type_error(message, error) if not self._on_argument_type_error_handlers: raise error for handler in self._on_argument_type_error_handlers: await handler(message, error) - async def _trigger_command_error(self, message: Message, error: errors.CommandError) -> None: + async def _trigger_command_error( + self, + message: Message, + error: errors.CommandError + ) -> None: await self.logger._trigger_command_error(message, error) for handler in self._on_command_error_handlers: await handler(message, error) - async def _trigger_command_permission_error(self, message: Message, error: errors.CommandPermissionError) -> None: + async def _trigger_command_permission_error( + self, + message: Message, + error: errors.CommandPermissionError + ) -> None: await self.logger._trigger_permissions_error(message, error) if not self._on_command_permission_error_handlers: raise error diff --git a/PyroArgs/types/logger.py b/PyroArgs/types/logger.py index d51f5d4..728661d 100644 --- a/PyroArgs/types/logger.py +++ b/PyroArgs/types/logger.py @@ -19,27 +19,32 @@ def __init__( self.permissions_error = False self.before_use_command_message = ( - 'User "{user}" wrote command "{command}" with args "{args}" and kwargs "{kwargs}".' + 'User "{user}" wrote command "{command}" with' + ' args "{args}" and kwargs "{kwargs}".' ) self.after_use_command_message = ( - 'User "{user}" used command "{command}" with args "{args}" and kwargs "{kwargs}".' + 'User "{user}" used command "{command}" with ' + 'args "{args}" and kwargs "{kwargs}".' ) self.missing_argument_error_message = ( 'Error in command "{command}" invoked by user "{user}": ' - 'Missing required argument "{missing_arg}" at position {arg_position}. ' + 'Missing required argument "{missing_arg}" ' + 'at position {arg_position}. ' 'Parsed arguments: {args}. Parsed keyword arguments: {kwargs}.' ) self.argument_type_error_message = ( 'Error in command "{command}" invoked by user "{user}": ' - 'Cannot convert argument "{missing_arg}" to "{required_type}" type at position {arg_position}. ' + 'Cannot convert argument "{missing_arg}" ' + 'to "{required_type}" type at position {arg_position}. ' 'Parsed arguments: {args}. Parsed keyword arguments: {kwargs}.' ) self.command_error_message = ( 'Error in command "{command}" used by "{user}": "{error}".' ) self.permissions_error_message = ( - 'Permission error: User "{user}" does not have permission level "{level}" to execute command "{command}".' + 'Permission error: User "{user}" does not have ' + 'permission level "{level}" to execute command "{command}".' ) self.logger = logging.getLogger('PyroArgs') @@ -66,9 +71,15 @@ def __get_username(self, message: Message) -> str: return f'@{message.from_user.username}' return message.from_user.first_name - def setup_logs(self, before_use_command: bool = True, after_use_command: bool = True, - missing_argument_error: bool = True, argument_type_error: bool = True, - command_error: bool = True, permissions_error: bool = True) -> None: + def setup_logs( + self, + before_use_command: bool = True, + after_use_command: bool = True, + missing_argument_error: bool = True, + argument_type_error: bool = True, + command_error: bool = True, + permissions_error: bool = True + ) -> None: self.before_use_command = before_use_command self.after_use_command = after_use_command self.missing_argument_error = missing_argument_error @@ -93,7 +104,11 @@ async def _trigger_before_use_command( )) async def _trigger_after_use_command( - self, message: Message, command: str, args: List[Any], kwargs: Dict[str, Any] + self, + message: Message, + command: str, + args: List[Any], + kwargs: Dict[str, Any] ) -> None: if self.after_use_command: self.logger.info(self.after_use_command_message.format( @@ -103,7 +118,11 @@ async def _trigger_after_use_command( kwargs=kwargs )) - async def _trigger_missing_argument_error(self, message: Message, error: errors.MissingArgumentError) -> None: + async def _trigger_missing_argument_error( + self, + message: Message, + error: errors.MissingArgumentError + ) -> None: if self.missing_argument_error: self.logger.info(self.missing_argument_error_message.format( user=self.__get_username(message), @@ -114,7 +133,11 @@ async def _trigger_missing_argument_error(self, message: Message, error: errors. arg_position=error.missing_arg_position )) - async def _trigger_argument_type_error(self, message: Message, error: errors.ArgumentTypeError) -> None: + async def _trigger_argument_type_error( + self, + message: Message, + error: errors.ArgumentTypeError + ) -> None: if self.argument_type_error: self.logger.info(self.argument_type_error_message.format( user=self.__get_username(message), @@ -126,7 +149,11 @@ async def _trigger_argument_type_error(self, message: Message, error: errors.Arg required_type=error.required_type )) - async def _trigger_command_error(self, message: Message, error: errors.CommandError) -> None: + async def _trigger_command_error( + self, + message: Message, + error: errors.CommandError + ) -> None: if self.command_error: self.logger.info(self.command_error_message.format( user=self.__get_username(message), @@ -136,7 +163,11 @@ async def _trigger_command_error(self, message: Message, error: errors.CommandEr error=error.original_error )) - async def _trigger_permissions_error(self, message: Message, error: errors.CommandPermissionError) -> None: + async def _trigger_permissions_error( + self, + message: Message, + error: errors.CommandPermissionError + ) -> None: if self.permissions_error: self.logger.info(self.permissions_error_message.format( user=self.__get_username(message), From 552c11cd62883b4e1b77e40d5f31447aef7ca792 Mon Sep 17 00:00:00 2001 From: DogiFnf Date: Sun, 6 Apr 2025 02:29:55 +0300 Subject: [PATCH 4/5] the parser has been restored --- PyroArgs/parser.py | 242 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 PyroArgs/parser.py diff --git a/PyroArgs/parser.py b/PyroArgs/parser.py new file mode 100644 index 0000000..e97793a --- /dev/null +++ b/PyroArgs/parser.py @@ -0,0 +1,242 @@ +from typing import ( + Dict, + Any, + List, + Callable, + Union, + Tuple +) +import inspect +import shlex +from . import errors + + +def get_command_and_args( + text: str, + prefixes: Union[List[str], Tuple[str], str] +) -> Tuple[str, str]: + """ + Gets command and arguments from a string. + + The function takes a string and a list/tuple of prefixes as parameters. + It first strips the string of any whitespaces, then checks if the string + starts with any of the prefixes. If not, it raises a NameError. + + Then it splits the string by spaces and takes the first part as the command. + It goes through the list of prefixes and checks if the command starts with + any of them. If it does, it removes the prefix from the command. + + Finally, it takes the rest of the string as the arguments, strips it of any + whitespaces and returns a tuple with the command and arguments. + + ## Parameters + text (``str``): + The string to parse. + prefixes : (``List[str]`` or ``Tuple[str]`` or ``str``): + The ``list/tuple`` or single prefix to check against. + + ## Returns + ``Tuple[str, str]``: The command and arguments as a tuple. + """ # noqa + text = text.strip() + + if not text.startswith(tuple(prefixes)): + raise NameError('Command does not start with prefix.') + + cmd = text.split()[0] + for prefix in prefixes: + if cmd.startswith(prefix): + cmd = cmd[len(prefix):] + break + + args = text[len(prefix)+len(cmd):].strip() + return cmd, args + + +def parse_command( + func: Callable, + args: str, + trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), + falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') +) -> Any: + """ + Executes the given function `func` with arguments parsed from the `command` string. + + ## Parameters + func (``Callable``): + The function to be executed. + + command (``str``): + The command string containing arguments for the function. + + trues (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as True. + + falses (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as False. + + ## Returns + ``Any``: The result of executing `func` with the parsed arguments. + + ## Raises + ``ValueError``: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. + """ # noqa + def get_bool( + arg: str, + trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), # noqa + falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') # noqa + ) -> bool: + """ + Converts a string argument to a boolean. + + ## Parameters + arg (``str``): + The argument to be converted. + + trues (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as True. + + falses (``Union[List[str], Tuple[str], str]``, *optional*): + A list or tuple of strings to interpret as False. + + ## Returns + ``bool``: The converted boolean value. + + ## Raises + ``ValueError``: If the argument is not in the trues or falses lists. + """ # noqa + if isinstance(trues, str): + trues = [trues] + if isinstance(falses, str): + falses = [falses] + if arg.lower() in trues: + return True + if arg.lower() in falses: + return False + raise ValueError( + f'Failed to cast argument "{arg}" to bool.') + + signature: inspect.Signature = inspect.signature(func) + lexer: shlex.shlex = shlex.shlex(args.strip(), posix=True) + lexer.whitespace_split = True + lexer.escapedquotes = '' + lexer.quotes = ' ' + lexer.whitespace = ' ' + lexer.commenters = '' + args_list: List[str] = list(lexer) + + args_counter: int = 0 + result_args: List[Any] = [] + result_kwargs: Dict[str, Any] = {} + is_keyword_only_used: bool = False + + for param in signature.parameters.values(): + if param.kind == param.VAR_POSITIONAL: + raise SyntaxError( + f'Positional var are not supported. Remove "*{param.name}".' + ) + elif param.kind == param.VAR_KEYWORD: + raise SyntaxError( + f'Keyword var are not supported. Remove "**{param.name}".' + ) + + for name, param in list(signature.parameters.items())[1:]: + # HINT: print(name, param.kind, param.default, param.annotation) + if param.kind in (param.POSITIONAL_ONLY, param.POSITIONAL_OR_KEYWORD): + default_used: bool = False + try: + arg: str = args_list[args_counter] + except IndexError: + if param.default != param.empty: + default_used = True + arg = param.default + result_args.append(arg) + else: + raise errors.MissingArgumentError( + name=name, + message_object=None, + missing_arg_name=name, + missing_arg_position=args_counter+1, + parsed_args=args, + parsed_kwargs=result_kwargs + ) + + if not default_used: + if param.annotation == bool: + arg = get_bool(arg, trues, falses) + + elif param.annotation != inspect._empty: + try: + if param.annotation != Any: + arg = param.annotation(arg) + except ValueError: + pass + raise errors.ArgumentTypeError( + name=name, + message_object=None, + parsed_args=args, + parsed_kwargs=result_kwargs, + errored_arg_name=name, + errored_arg_position=args_counter+1, + required_type=param.annotation + ) + result_args.append(arg) + + elif param.kind == param.KEYWORD_ONLY: + if is_keyword_only_used: + raise SyntaxError( + 'There should not be more than one keyword argument in the function call.' # noqa + ) + is_keyword_only_used = True + + arg: str = ( + args.split(args_list[args_counter-1], 1)[1] + if args_counter > 0 + else args + ) + + if not arg: + if param.default != param.empty: + arg = param.default + + if param.annotation == bool: + arg = get_bool(arg, trues, falses) + + elif param.annotation != inspect._empty: + try: + if param.annotation != Any: + if arg == '': + arg = param.annotation() + else: + arg = param.annotation(arg) + except ValueError: + pass + raise errors.ArgumentTypeError( + name=name, + message_object=None, + parsed_args=args, + parsed_kwargs=result_kwargs, + errored_arg_name=name, + errored_arg_position=args_counter+1, + required_type=param.annotation + ) + result_kwargs[name] = arg + + args_counter += 1 + + # return func(*result_args, **result_kwargs) + return result_args, result_kwargs + + +if __name__ == '__main__': + def func(a: str, b: bool = '52') -> None: + print(a, b, sep='\n') + print(type(a), type(b), sep='\n') + return 'RESULT_AR' + + text = '/test 111 true' + cmd, args = get_command_and_args(text, ['/', 'v?']) + + if cmd == 'test': + result_args, result_kwargs = parse_command(func, args) + print(func(*result_args, **result_kwargs)) From 55f16c217b0e9959c55d7a4fdba3dc8f11af0625 Mon Sep 17 00:00:00 2001 From: vo0ov Date: Mon, 7 Apr 2025 16:00:17 +0300 Subject: [PATCH 5/5] =?UTF-8?q?1.=20=D0=9F=D0=BE=D0=B2=D1=8B=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BE=201.4=202.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BB=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B0=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2=203.=20?= =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?README.md=20=D0=B4=D0=BB=D1=8F=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=B5=D0=B9=20=D1=8F=D1=81=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BA=D0=B5=204.=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88?= =?UTF-8?q?=D0=B8=D1=85=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80-=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D0=BE=D0=B2=20=D0=B2=20ful?= =?UTF-8?q?l=5Fexample=5Fbot.py=205.=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D0=B0=D1=80=D1=8B=D1=85=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BE=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=B8=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82?= =?UTF-8?q?=D1=83=D1=80=D1=8B=206.=20=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20DataHolder.py=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20PyroArgsObj=20=D0=B8=20ClientObj=207.=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20.flake8=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=B5=D1=81=D0=BF=D0=B5=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 9 + .github/workflows/publish-to-pypi.yml | 47 +-- .github/workflows/publish-to-test-pypi.yml | 44 +-- .github/workflows/python-lint-and-tests.yml | 24 +- .gitignore | 294 +++++++++++++++++- LICENSE | 4 +- PyroArgs/__init__.py | 6 +- PyroArgs/errors/CommandPermissionError.py | 6 +- PyroArgs/parser.py | 185 +++++------ PyroArgs/pyroargs.py | 31 +- PyroArgs/types/logger.py | 8 +- PyroArgs/utils/DataHolder.py | 6 +- README.md | 110 +++---- examples/all_events.py | 53 ---- examples/auto_generate_help.py | 23 -- examples/auto_type.py | 24 -- examples/basic_example.py | 24 -- examples/command_info.py | 37 --- examples/custom_data_example.py | 16 - examples/error_handling.py | 28 -- examples/filter_example.py | 16 - examples/full_example_bot.py | 234 ++++++++++++++ examples/permissions_example.py | 33 -- examples/say_command.py | 17 - setup.py | 10 +- tests/test_errors/test_ArgumentTypeError.py | 30 -- tests/test_errors/test_ArgumentsError.py | 24 -- tests/test_errors/test_CommandError.py | 27 -- .../test_CommandPermissionError.py | 24 -- .../test_errors/test_MissingArgumentError.py | 28 -- tests/test_parser.py | 102 ------ tests/test_pyroargs.py | 51 --- tests/test_types/test_command.py | 40 --- tests/test_types/test_commandRegistry.py | 61 ---- tests/test_types/test_events.py | 40 --- tests/test_types/test_logger.py | 0 36 files changed, 738 insertions(+), 978 deletions(-) create mode 100644 .flake8 delete mode 100644 examples/all_events.py delete mode 100644 examples/auto_generate_help.py delete mode 100644 examples/auto_type.py delete mode 100644 examples/basic_example.py delete mode 100644 examples/command_info.py delete mode 100644 examples/custom_data_example.py delete mode 100644 examples/error_handling.py delete mode 100644 examples/filter_example.py create mode 100644 examples/full_example_bot.py delete mode 100644 examples/permissions_example.py delete mode 100644 examples/say_command.py delete mode 100644 tests/test_errors/test_ArgumentTypeError.py delete mode 100644 tests/test_errors/test_ArgumentsError.py delete mode 100644 tests/test_errors/test_CommandError.py delete mode 100644 tests/test_errors/test_CommandPermissionError.py delete mode 100644 tests/test_errors/test_MissingArgumentError.py delete mode 100644 tests/test_parser.py delete mode 100644 tests/test_pyroargs.py delete mode 100644 tests/test_types/test_command.py delete mode 100644 tests/test_types/test_commandRegistry.py delete mode 100644 tests/test_types/test_events.py delete mode 100644 tests/test_types/test_logger.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..c1acb67 --- /dev/null +++ b/.flake8 @@ -0,0 +1,9 @@ +[flake8] +; * Основные настройки * ; +max-line-length = 127 + +; * Кавычки * ; +inline-quotes = single +docstring-quotes = double +multiline-quotes = single +avoid-escape = True diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index ba7efe5..4e49526 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -1,33 +1,34 @@ name: Publish to PyPI on: - release: - types: [created] + workflow_dispatch: + release: + types: [created] jobs: - build-and-publish: - runs-on: ubuntu-latest + build-and-publish: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine - - name: Build package - run: | - python -m build + - name: Build package + run: | + python -m build - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python -m twine upload dist/* + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m twine upload dist/* diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index dcc2a40..ffcc784 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -1,32 +1,32 @@ name: Publish to TestPyPI on: - workflow_dispatch: + workflow_dispatch: jobs: - build-and-publish: - runs-on: ubuntu-latest + build-and-publish: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 + steps: + - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build twine + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build twine - - name: Build package - run: | - python -m build + - name: Build package + run: | + python -m build - - name: Publish package to TestPyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} - run: | - python -m twine upload --repository testpypi dist/* + - name: Publish package to TestPyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} + run: | + python -m twine upload --repository testpypi dist/* diff --git a/.github/workflows/python-lint-and-tests.yml b/.github/workflows/python-lint-and-tests.yml index fb17c4c..1be3c09 100644 --- a/.github/workflows/python-lint-and-tests.yml +++ b/.github/workflows/python-lint-and-tests.yml @@ -3,36 +3,28 @@ name: Python Package on: workflow_dispatch: push: - branches: - - main + pull_request: jobs: - tests: + lint: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pyrogram tgcrypto pytest pytest-asyncio mock - shell: bash + pip install flake8 flake8-quotes bandit - - name: Lint with flake8 + - name: Run flake8 run: | - # Stop the build if there are Python syntax errors or undefined names - flake8 PyroArgs tests --count --select=E9,F63,F7,F82 --show-source --statistics - # Exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 PyroArgs tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - shell: bash + flake8 . - - name: Run tests + - name: Run bandit run: | - python -m unittest discover tests - shell: bash + bandit -r . diff --git a/.gitignore b/.gitignore index 0534bf6..d190152 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,290 @@ -# Python +# Created by https://www.toptal.com/developers/gitignore/api/python,dotenv,visualstudiocode,intellij+all +# Edit at https://www.toptal.com/developers/gitignore?templates=python,dotenv,visualstudiocode,intellij+all + +### dotenv ### +.env + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Python ### +# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] -*.egg-info/ +*$py.class -# Build +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -# Pyrogram -*.session* +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -# Editor -.vscode/ +# Installer logs +pip-log.txt +pip-delete-this-directory.txt -# Others -HELP.md -test_bot.py -config.py -docs/ +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ .pytest_cache/ -temp.py +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,dotenv,visualstudiocode,intellij+all + +# Others +UPDATE HELP.md +*.session* +*test*.py diff --git a/LICENSE b/LICENSE index 0f9ce4a..0d9b232 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ -Copyright (c) 2024, vo0ov +# Copyright (c) 2024, vo0ov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PyroArgs/__init__.py b/PyroArgs/__init__.py index bbd59b2..70e1418 100644 --- a/PyroArgs/__init__.py +++ b/PyroArgs/__init__.py @@ -1,7 +1,7 @@ # PyroArgs/__init__.py -from . import types, errors -from .utils import DataHolder +from . import errors, types from .pyroargs import PyroArgs +from .utils import DataHolder __all__ = ['PyroArgs', 'types', 'errors', 'DataHolder'] -__version__ = '1.3' # ВЕРСИЯ +__version__ = '1.4' # ВЕРСИЯ diff --git a/PyroArgs/errors/CommandPermissionError.py b/PyroArgs/errors/CommandPermissionError.py index d41ba92..0d11e3d 100644 --- a/PyroArgs/errors/CommandPermissionError.py +++ b/PyroArgs/errors/CommandPermissionError.py @@ -5,13 +5,13 @@ class CommandPermissionError(Exception): def __init__( self, - name: str, + command: str, message: Message, permission_level: int ): full_message = ('Permissions error: User does not ' - f'have permission to use command "{name}".') + f'have permission to use command "{command}".') super().__init__(full_message) - self.name = name + self.command = command self.message = message self.permission_level = permission_level diff --git a/PyroArgs/parser.py b/PyroArgs/parser.py index e97793a..214fde2 100644 --- a/PyroArgs/parser.py +++ b/PyroArgs/parser.py @@ -1,20 +1,11 @@ -from typing import ( - Dict, - Any, - List, - Callable, - Union, - Tuple -) import inspect import shlex +from typing import Any, Callable, Dict, List, Tuple, Union + from . import errors -def get_command_and_args( - text: str, - prefixes: Union[List[str], Tuple[str], str] -) -> Tuple[str, str]: +def get_command_and_args(text: str, prefixes: Union[List[str], Tuple[str], str]) -> Tuple[str, str]: """ Gets command and arguments from a string. @@ -29,15 +20,18 @@ def get_command_and_args( Finally, it takes the rest of the string as the arguments, strips it of any whitespaces and returns a tuple with the command and arguments. - ## Parameters - text (``str``): - The string to parse. - prefixes : (``List[str]`` or ``Tuple[str]`` or ``str``): - The ``list/tuple`` or single prefix to check against. - - ## Returns - ``Tuple[str, str]``: The command and arguments as a tuple. - """ # noqa + Parameters + ---------- + text : str + The string to parse. + prefixes : List[str] or Tuple[str] or str + The list/tuple or single prefix to check against. + + Returns + ------- + Tuple[str, str] + The command and arguments as a tuple. + """ text = text.strip() if not text.startswith(tuple(prefixes)): @@ -54,74 +48,30 @@ def get_command_and_args( def parse_command( - func: Callable, - args: str, - trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), - falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') + func: Callable, args: str ) -> Any: """ Executes the given function `func` with arguments parsed from the `command` string. - ## Parameters - func (``Callable``): - The function to be executed. - - command (``str``): - The command string containing arguments for the function. - - trues (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as True. - - falses (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as False. - - ## Returns - ``Any``: The result of executing `func` with the parsed arguments. - - ## Raises - ``ValueError``: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. - """ # noqa - def get_bool( - arg: str, - trues: Union[List[str], Tuple[str], str] = ('true', 'yes', 'y', 't'), # noqa - falses: Union[List[str], Tuple[str], str] = ('false', 'no', 'n', 'f') # noqa - ) -> bool: - """ - Converts a string argument to a boolean. - - ## Parameters - arg (``str``): - The argument to be converted. - - trues (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as True. - - falses (``Union[List[str], Tuple[str], str]``, *optional*): - A list or tuple of strings to interpret as False. - - ## Returns - ``bool``: The converted boolean value. - - ## Raises - ``ValueError``: If the argument is not in the trues or falses lists. - """ # noqa - if isinstance(trues, str): - trues = [trues] - if isinstance(falses, str): - falses = [falses] - if arg.lower() in trues: - return True - if arg.lower() in falses: - return False - raise ValueError( - f'Failed to cast argument "{arg}" to bool.') + Args: + func (Callable): The function to be executed. + command (str): The command string containing arguments for the function. + trues (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as True. + falses (Union[List[str], Tuple[str], str], optional): A list or tuple of strings to interpret as False. + + Returns: + Any: The result of executing `func` with the parsed arguments. + + Raises: + ValueError: If a parameter is missing, casting fails, or multiple keyword-only arguments are used. + """ signature: inspect.Signature = inspect.signature(func) lexer: shlex.shlex = shlex.shlex(args.strip(), posix=True) lexer.whitespace_split = True - lexer.escapedquotes = '' - lexer.quotes = ' ' - lexer.whitespace = ' ' + lexer.escapedquotes = '"' + lexer.quotes = '"' + lexer.whitespace = ' \n' lexer.commenters = '' args_list: List[str] = list(lexer) @@ -157,51 +107,53 @@ def get_bool( message_object=None, missing_arg_name=name, missing_arg_position=args_counter+1, - parsed_args=args, + parsed_args=args_list, parsed_kwargs=result_kwargs ) if not default_used: - if param.annotation == bool: - arg = get_bool(arg, trues, falses) - - elif param.annotation != inspect._empty: + if param.annotation != inspect._empty: try: if param.annotation != Any: arg = param.annotation(arg) except ValueError: - pass raise errors.ArgumentTypeError( name=name, message_object=None, - parsed_args=args, + parsed_args=args_list, parsed_kwargs=result_kwargs, errored_arg_name=name, errored_arg_position=args_counter+1, required_type=param.annotation - ) + ) from None result_args.append(arg) elif param.kind == param.KEYWORD_ONLY: if is_keyword_only_used: raise SyntaxError( - 'There should not be more than one keyword argument in the function call.' # noqa + 'There should not be more than one keyword argument in the function call.' ) is_keyword_only_used = True - arg: str = ( - args.split(args_list[args_counter-1], 1)[1] - if args_counter > 0 - else args - ) + arg = '' + if args_counter < len(args_list): + arg: str = ( + args.split(args_list[args_counter-1], 1)[1] + if args_counter > 0 + else args + ) + elif args_counter > 0: + try: + parts = args.split(args_list[args_counter - 1], 1) + if len(parts) > 1: + arg = parts[1].strip() + except (IndexError, ValueError): + pass if not arg: if param.default != param.empty: arg = param.default - if param.annotation == bool: - arg = get_bool(arg, trues, falses) - elif param.annotation != inspect._empty: try: if param.annotation != Any: @@ -210,33 +162,38 @@ def get_bool( else: arg = param.annotation(arg) except ValueError: - pass raise errors.ArgumentTypeError( name=name, message_object=None, - parsed_args=args, + parsed_args=args_list, parsed_kwargs=result_kwargs, errored_arg_name=name, errored_arg_position=args_counter+1, required_type=param.annotation - ) - result_kwargs[name] = arg + ) from None + result_kwargs[name] = arg.strip() args_counter += 1 - # return func(*result_args, **result_kwargs) return result_args, result_kwargs if __name__ == '__main__': - def func(a: str, b: bool = '52') -> None: - print(a, b, sep='\n') - print(type(a), type(b), sep='\n') - return 'RESULT_AR' - - text = '/test 111 true' - cmd, args = get_command_and_args(text, ['/', 'v?']) - - if cmd == 'test': - result_args, result_kwargs = parse_command(func, args) - print(func(*result_args, **result_kwargs)) + def func(message: ..., user: str, ban_time: int = 120, *, reason: str): + print('---') + print(user) + print('---') + print(ban_time) + print('---') + print(reason) + print('---') + + print(type(user)) + print(type(ban_time)) + print(type(reason)) + + args = 'Notch -1 X-Ray' + + result_args, result_kwargs = parse_command(func, args) + print(result_args, result_kwargs) + func(..., *result_args, **result_kwargs) diff --git a/PyroArgs/pyroargs.py b/PyroArgs/pyroargs.py index 3077693..edbfc02 100644 --- a/PyroArgs/pyroargs.py +++ b/PyroArgs/pyroargs.py @@ -1,17 +1,17 @@ # PyroArgs/pyroargs.py -from typing import Callable, List, Any, TypeVar, Tuple, Dict, Optional, Union -from pyrogram.filters import Filter, create, command -from pyrogram.handlers import MessageHandler +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union + from pyrogram import Client +from pyrogram.filters import Filter, command, create +from pyrogram.handlers import MessageHandler +from . import errors from .parser import get_command_and_args, parse_command -from .types.commandRegistry import CommandRegistry +from .types import Message from .types.command import Command +from .types.commandRegistry import CommandRegistry from .types.events import Events from .utils import DataHolder -from .types import Message -from . import errors - F = TypeVar('F', bound=Callable[..., Any]) @@ -29,7 +29,7 @@ def __init__( self.events: Events = Events(log_file) self.registry: CommandRegistry = CommandRegistry() self.permission_checker_func: Callable[[ - int, Message], bool] = None + Message, int], bool] = None # Сохраняем объекты для доступа в плагинах DataHolder.ClientObj = self.bot @@ -145,11 +145,16 @@ async def __parse_arguments( await self.events._trigger_argument_type_error(message, e) except Exception as e: print( - '!!! PYROARGS ERROR !!!', - 'PLEASE REPORT THIS ERROR:', - 'https://github.com/vo0ov/PYPI-PyroArgs/issues', - '!!! PYROARGS ERROR !!!', - sep='\n' + ( + '\n' + '!!!!!!!!! PYROARGS CRITICAL ERROR !!!!!!!!!\n' + '!! !!\n' + '!! PLEASE REPORT THIS ERROR: !!\n' + '!! https://github.com/vo0ov/PYPI-PyroArgs/issues !!\n' + '!! !!\n' + '!!!!!!!!! PYROARGS CRITICAL ERROR !!!!!!!!!\n' + '\n' + ) ) raise SystemError(e) return None diff --git a/PyroArgs/types/logger.py b/PyroArgs/types/logger.py index 728661d..973710d 100644 --- a/PyroArgs/types/logger.py +++ b/PyroArgs/types/logger.py @@ -1,9 +1,9 @@ # PyroArgs/types/logger.py import logging -from typing import List, Dict, Any +from typing import Any, Dict, List -from . import Message from .. import errors +from . import Message class Logger: @@ -157,7 +157,7 @@ async def _trigger_command_error( if self.command_error: self.logger.info(self.command_error_message.format( user=self.__get_username(message), - command=error.name, + command=error.command, args=error.parsed_args, kwargs=error.parsed_kwargs, error=error.original_error @@ -171,6 +171,6 @@ async def _trigger_permissions_error( if self.permissions_error: self.logger.info(self.permissions_error_message.format( user=self.__get_username(message), - command=error.name, + command=error.command, level=error.permission_level )) diff --git a/PyroArgs/utils/DataHolder.py b/PyroArgs/utils/DataHolder.py index 4cdf8c9..1515ece 100644 --- a/PyroArgs/utils/DataHolder.py +++ b/PyroArgs/utils/DataHolder.py @@ -1,8 +1,8 @@ # PyroArgs/utils/DataHolder.py -from ..pyroargs import PyroArgs -from pyrogram import Client from typing import Any -PyroArgsObj: PyroArgs = None +from pyrogram import Client + +PyroArgsObj = None ClientObj: Client = None CustomData: Any = None diff --git a/README.md b/README.md index 0794807..755d5d3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + ![PyPI - Downloads](https://img.shields.io/pypi/dm/PyroArgs?label=%D0%A1%D0%BA%D0%B0%D1%87%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D0%B9) ![PyPI - License](https://img.shields.io/pypi/l/PyroArgs?label=%D0%9B%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F) # [PyroArgs на PyPi](https://pypi.org/project/PyroArgs/) @@ -6,19 +7,35 @@ ## Особенности -- **Удобный декоратор для создания команд** с поддержкой позиционных и именованных аргументов. -- **Обработка ошибок аргументов и команд** с помощью специальных исключений `ArgumentsError` и `CommandError`. -- **Система событий** для регистрации обработчиков ошибок. -- **Поддержка `custom_data`** для передачи дополнительных данных в команды. -- **Совместимость с пользовательскими фильтрами и группами обработчиков** из Pyrogram. +- **Удобный декоратор для создания команд** с поддержкой позиционных и именованных аргументов прямо как в библиотеке `Discord.py` +- **Обработка ошибок аргументов и команд** с помощью специальных исключений-ивентов +- **Система событий** для регистрации обработчиков ошибок и событий +- **Поддержка `command_meta_data`** для передачи дополнительных данных в команды +- **Совместимость с пользовательскими фильтрами и группами обработчиков** из Pyrogram ## Установка +- Полная установка `PyroArgs` с использованием `pip` (Лучший вариант): + +```bash +pip install PyroArgs[all] +``` + +- Установка `PyroArgs` с использованием `pip` без `TgCrypto` (Это не безопасно!): + ```bash pip install PyroArgs ``` -## Использование +- Установка `PyroArgs` с использованием через `git` (Для продвинутых пользователей): + +```bash +git clone https://github.com/vo0ov/PyroArgs.git +cd PyroArgs +pip install . +``` + +## Использование библиотеки ### Импорт необходимых модулей @@ -30,90 +47,63 @@ from PyroArgs import PyroArgs, types, errors ### Инициализация клиента и `PyroArgs` ```python -# Инициализируйте клиент Pyrogram с вашими учетными данными -bot = Client("my_bot", api_id=..., api_hash="...") # Замените '...' на ваши api_id и api_hash +# Замените 12345 и 'abcdef' на ваши api_id и api_hash в этой строке, НО лучше использовать переменные окружения +bot = Client('Bot', api_id=12345, api_hash='abcdef') -# Создайте экземпляр PyroArgs с префиксами для команд -pyro_args = PyroArgs(bot, prefixes=["/"]) +# Создание экземпляра PyroArgs с префиксом команд +pyro_args = PyroArgs(bot, prefixes=['/']) ``` -### Создание команды +### Создание команд ```python @pyro_args.command() async def greet(message: types.Message, name: str): - await message.reply(f"Привет, {name}!") -``` + await message.reply(f'Привет, {name}!') -### Обработка ошибок аргументов - -```python -@pyro_args.events.on_arguments_error -async def handle_arguments_error(message: types.Message, error: errors.ArgumentsError): - await message.reply(f"Ошибка аргументов: {error}") -``` - -### Обработка ошибок команд - -```python -@pyro_args.events.on_command_error -async def handle_command_error(message: types.Message, error: errors.CommandError): - await message.reply(f"Ошибка в команде: {error}") +@pyro_args.command() +async def echo(message: types.Message, *, text: str): + await message.reply(text if text else '❌ Нельзя отправить пустой текст!') ``` ### Запуск бота ```python -if __name__ == "__main__": +if __name__ == '__main__': bot.run() ``` -## Полный код из примера выше: +## Полный код из примера выше ```python from pyrogram import Client from PyroArgs import PyroArgs, types, errors -# Заполните ваши api_id и api_hash -bot = Client("my_bot", api_id=..., api_hash="...") # Замените '...' на ваши api_id и api_hash +# Замените 12345 и 'abcdef' на ваши api_id и api_hash в этой строке, НО лучше использовать переменные окружения +bot = Client('Bot', api_id=12345, api_hash='abcdef') # Создание экземпляра PyroArgs с префиксом команд -pyro_args = PyroArgs(bot, prefixes=["/"]) +pyro_args = PyroArgs(bot, prefixes=['/']) -# Создание команды +# Создание просейшей команды @pyro_args.command() async def greet(message: types.Message, name: str): - await message.reply(f"Привет, {name}!") - -# Обработка ошибок аргументов -@pyro_args.events.on_arguments_error -async def handle_arguments_error(message: types.Message, error: errors.ArgumentsError): - await message.reply(f"Ошибка аргументов: {error}") + await message.reply(f'Привет, {name}!') -# Обработка ошибок команд -@pyro_args.events.on_command_error -async def handle_command_error(message: types.Message, error: errors.CommandError): - await message.reply(f"Ошибка в команде: {error}") +@pyro_args.command() +async def echo(message: types.Message, *, text: str): + await message.reply(text if text else '❌ Нельзя отправить пустой текст!') # Запуск бота -if __name__ == "__main__": +if __name__ == '__main__': bot.run() ``` -## Примеры +## Пример использования большенства функций `PyroArgs` -В папке `examples/` представлены различные примеры использования библиотеки PyroArgs: +В папке `examples/` присутствуют полный пример использования `PyroArgs`.: -- [`greet_bot.py`](examples/greet_bot.py): Базовая команда приветствия. -- [`private_info_bot.py`](examples/private_info_bot.py): Использование `custom_data` и пользовательского фильтра. -- [`argument_error_bot.py`](examples/argument_error_bot.py): Обработка ошибки аргументов. -- [`command_error_bot.py`](examples/command_error_bot.py): Обработка исключений внутри команд. -- [`echo_bot.py`](examples/echo_bot.py): Команда с позиционными и именованными аргументами. -- [`admin_only_bot.py`](examples/admin_only_bot.py): Команда доступна только администраторам. -- [`version_bot.py`](examples/version_bot.py): Использование `custom_data` для передачи версии бота. -- [`join_args_bot.py`](examples/join_args_bot.py): Команда с произвольным количеством аргументов. -- [`set_setting_bot.py`](examples/set_setting_bot.py): Команда с именованным аргументом после `*`. -- [`full_featured_bot.py`](examples/full_featured_bot.py): Комплексный бот с полной обработкой ошибок. +- [`full_example_bot.py`](examples/full_example_bot.py) - полный пример использования `PyroArgs`. ## Вклад в проект @@ -126,15 +116,15 @@ if __name__ == "__main__": ## Лицензия -Этот проект распространяется под лицензией MIT. Подробности см. в файле [`LICENSE`](LICENSE) или ниже: +Этот проект распространяется под лицензией MIT. Подробности смотрите в файле [`LICENSE`](LICENSE) или ниже: -``` +```License MIT License Copyright (c) 2024, vo0ov -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` diff --git a/examples/all_events.py b/examples/all_events.py deleted file mode 100644 index 1543d0b..0000000 --- a/examples/all_events.py +++ /dev/null @@ -1,53 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types, errors - -# Initialize client and PyroArgs -app = Client("auto_type_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# This event is called before the command is used -@PyAr.events.on_before_use_command -async def on_before_use_command(message: types.Message, command: str, args: list, kwargs: dict): - await message.reply(f'Before command {command} used!', quote=True) - - -# This event is called after the command is used -@PyAr.events.on_after_use_command -async def on_after_use_command(message: types.Message, command: str, args: list, kwargs: dict): - await message.reply(f'Command {command} used!', quote=True) - - -# This event is called when a missing argument error occurs -@PyAr.events.on_missing_argument_error -async def on_missing_argument_error(message: types.Message, error: errors.MissingArgumentError): - await message.reply(f'Missing argument error! Error: {error}', quote=True) - - -# This event is called when an argument type error occurs -@PyAr.events.on_argument_type_error -async def on_argument_type_error(message: types.Message, error: errors.ArgumentTypeError): - await message.reply(f'Argument type error! Error: {error}', quote=True) - - -# This event is called when a command error occurs -@PyAr.events.on_command_error -async def on_command_error(message: types.Message, error: errors.CommandError): - await message.reply('Command error!', quote=True) - - -# This event is called when a command permission error occurs -@PyAr.events.on_command_permission_error -async def on_command_permission_error(message: types.Message, error: errors.CommandPermissionError): - await message.reply('Command permission error!', quote=True) - - -# Raise a command error -@PyAr.command() -async def raise_error(message: types.Message): - 1/0 # This will raise a ZeroDivisionError - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/auto_generate_help.py b/examples/auto_generate_help.py deleted file mode 100644 index 64d8f9c..0000000 --- a/examples/auto_generate_help.py +++ /dev/null @@ -1,23 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("auto_type_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'help' with auto generated help -@PyAr.command('help', 'Список команд', category='🔧 Основные') -async def help(message: types.Message): - text = '**📚 Список команд:\n**' - for category, cmds in PyAr.registry.iterate_categories_with_commands(): - text += f'\n**{category}**:\n' - for cmd in cmds: - text += f'• `v?{cmd.command}` - {cmd.description}\n' - - await message.reply(text, quote=True) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/auto_type.py b/examples/auto_type.py deleted file mode 100644 index ab26590..0000000 --- a/examples/auto_type.py +++ /dev/null @@ -1,24 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("auto_type_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'example' with auto typed arguments -@PyAr.command() -async def example(message: types.Message, arg1: int, arg2: float): - # True. (Auto converted str from message text to int) - print(isinstance(arg1, int)) - - # True (Auto converted str from message text to float) - print(isinstance(arg2, float)) - - # Send a reply - await message.reply(f"arg1: {arg1}, arg2: {arg2}", quote=True) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/basic_example.py b/examples/basic_example.py deleted file mode 100644 index 32a8307..0000000 --- a/examples/basic_example.py +++ /dev/null @@ -1,24 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - - -# Initialize client and PyroArgs -app = Client("basic_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'hello' with positional arguments -@PyAr.command() -async def hello(message: types.Message, name: str, age: int): - await message.reply(f"Hello, {name}! You are {age} years old.") - - -# Command 'sum' with named arguments -@PyAr.command() -async def sum(message: types.Message, a: int, b: int): - await message.reply(f"{a} + {b} = {a + b}", quote=True) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/command_info.py b/examples/command_info.py deleted file mode 100644 index 201c9b2..0000000 --- a/examples/command_info.py +++ /dev/null @@ -1,37 +0,0 @@ -from pyrogram import Client, filters -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("auto_type_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'example' with auto typed arguments -@PyAr.command('example', # Add command name - description='Example command', # Add command description - usage='example ', # Add command usage - example='example 1 2.0', # Add command example usage - permissions_level=999, # Check in PyAr.permissions_checker decorator - aliases=['ex'], # Add command aliases - category='General', # Add command category - command_meta_data={'key': 'value'}, # Add command meta data - filters=filters.private, # Add private filter - group=0 # Add group - ) -async def example(message: types.Message, arg1: int, arg2: float): - # True. (Auto converted str from message text to int) - print(isinstance(arg1, int)) - - # True (Auto converted str from message text to float) - print(isinstance(arg2, float)) - - # Get command_meta_data - print(message.command_meta_data) - - # Send a reply - await message.reply(f"arg1: {arg1}, arg2: {arg2}, command_meta_data: {message.command_meta_data}", quote=True) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/custom_data_example.py b/examples/custom_data_example.py deleted file mode 100644 index 60a9330..0000000 --- a/examples/custom_data_example.py +++ /dev/null @@ -1,16 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("custom_data_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'version' with custom data -@PyAr.command() -async def version(message: types.Message, version: str = "1.0"): - await message.reply(f"Version: {version}", quote=True) - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/error_handling.py b/examples/error_handling.py deleted file mode 100644 index 113682a..0000000 --- a/examples/error_handling.py +++ /dev/null @@ -1,28 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types, errors - - -# Initialize client and PyroArgs -app = Client("error_handling", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'divide' with error handling -@PyAr.command() -async def divide(message: types.Message, a: float, b: float): - try: - await message.reply(f"{a} / {b} = {a / b}") - except ZeroDivisionError: - raise errors.CommandError("Division by zero is not possible!") - - -# Command error handler -@PyAr.events.on_command_error -async def on_command_error(message: types.Message, error: errors.CommandError): - text = f'{message.from_user.first_name}, error in command "{error.name}": {error.original_error}! Sorry.' - await message.reply(text) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/filter_example.py b/examples/filter_example.py deleted file mode 100644 index aed6752..0000000 --- a/examples/filter_example.py +++ /dev/null @@ -1,16 +0,0 @@ -from pyrogram import Client, filters -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("filter_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'hello' with private filter -@PyAr.command(filters=filters.private) -async def hello(message: types.Message, name: str): - await message.reply(f"Hello, {name}!") - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/full_example_bot.py b/examples/full_example_bot.py new file mode 100644 index 0000000..f4343f9 --- /dev/null +++ b/examples/full_example_bot.py @@ -0,0 +1,234 @@ +from os import environ + +from dotenv import load_dotenv +from pyrogram import Client + +import PyroArgs + +# Загрузка переменных окружения +load_dotenv() + +##################### +# # +# НАСТРОйКИ # +# # +##################### + +# Инициализация клиента и PyroArgs +app = Client( + environ['AUTH_NAME'], + int(environ['AUTH_API_ID']), + environ['AUTH_API_HASH'], + bot_token=environ['BOT_TOKEN'] +) +PyAr = PyroArgs.PyroArgs(app, ('/')) + +# Включение логирования в консоль +# PyAr.setup_logs( +# before_use_command=True, +# after_use_command=True, +# missing_argument_error=True, +# argument_type_error=True, +# command_error=True, +# permissions_error=True +# ) + + +# Специальная функция для проверки права пользователя +@PyAr.permissions_checker +async def check( + message: PyroArgs.types.Message, + required_permission: int +): + # * У вас может быть своя функция для проверки прав, это лишь пример * # + + # Списки пользователей с правами + admninistrators = [123456789] + moderators = [987654321] + + # Не трогайте, если не знаете как это работает + user_id = message.from_user.id + current_permission = 0 + + # Проверяем, является ли пользователь модератором + if user_id in moderators: + current_permission = 100 # Модератор, уровень прав 100 + + # Проверяем, является ли пользователь администратором + elif user_id in admninistrators: + current_permission = 999 # Администратор, уровень прав 999 + + # Возвращаем True, если у пользователя достаточно прав + return current_permission >= required_permission + +#################### +# # +# ИВЕНТЫ # +# # +#################### + + +# Вызывается до выполнения кода в команде +@PyAr.events.on_before_use_command +async def on_before_use_command( + client: Client, + command: str, + args: list, + kwargs: dict +): + # * У вас может быть своя функция логирования, это лишь пример * # + + print(f'⏱️ Команда "{command}" начала выполнение...') + + +# Вызывается после выполнения кода в команде +@PyAr.events.on_after_use_command +async def on_after_use_command( + client: Client, + command: str, + args: list, + kwargs: dict +): + # * У вас может быть своя функция логирования, это лишь пример * # + + print(f'✅ Команда "{command}" завершила выполнение!') + + +# Вызывается при недостаточном количестве аргументов +@PyAr.events.on_missing_argument_error +async def on_missing_argument_error( + message: PyroArgs.types.Message, + error: PyroArgs.errors.MissingArgumentError +): + # * У вас может быть своя функция логирования, это лишь пример * # + + await message.reply(f'❌ Вы пропустили аргумент: `{error.name}`!', quote=True) + + +# Вызывается при неверном типе аргумента +@PyAr.events.on_argument_type_error +async def on_argument_type_error( + message: PyroArgs.types.Message, + error: PyroArgs.errors.ArgumentTypeError +): + # * У вас может быть своя функция логирования, это лишь пример * # + + await message.reply(f'❌ Неверный тип аргумента: `{error.name}`!', quote=True) + + +# Вызывается при возникновении ошибки в команде +@PyAr.events.on_command_error +async def on_command_error( + message: PyroArgs.types.Message, + error: PyroArgs.errors.CommandError +): + # * У вас может быть своя функция логирования, это лишь пример * # + + await message.reply(f'❌ Произошла ошибка в команде: `{error.command}`!', quote=True) + + +# Вызывается при недостаточном количестве прав у пользователя +@PyAr.events.on_command_permission_error +async def on_command_permission_error( + message: PyroArgs.types.Message, + error: PyroArgs.errors.CommandPermissionError +): + # * У вас может быть своя функция логирования, это лишь пример * # + + await message.reply(f'❌ Недостаточно прав для выполнения команды: `{error.command}`!', quote=True) + + +################### +# # +# КОМАНДЫ # +# # +################### + + +# Команда вывода списка всех доступных команд +@PyAr.command( + description='Выводит список всех доступных команд', + usage='/help', + example='/help', +) +async def help(message: PyroArgs.types.Message): + # Заголовок + help_text = 'Список доступных команд:\n' + + # Перебираем все категории + for category, commands in PyAr.registry.iterate_categories_with_commands(): + # Добавляем категорию + help_text += f'**Категория: {category}**\n' + + # Перебираем все команды + for cmd in commands: + # Если у пользователя достаточно прав, то добавляем команду + if await PyAr.permission_checker_func(message, cmd.permissions): + # Добавляем команду + help_text += f'/**{cmd.command}** - `{cmd.description}`\n' + help_text += f'Использование: `{cmd.usage}`\n' + help_text += f'Пример: `{cmd.example}`\n\n' + + # Отправляем сообщение + await message.reply(help_text) + + +# Команда для повторения текста +@PyAr.command( + description='Повторяет текст', + usage='/echo [текст]', + example='/echo Привет!', +) +async def echo(message: PyroArgs.types.Message, *, text: str): + # Проверяем текст на пустоту + if not text: + # Если текст пустой, то отправляем сообщение об ошибке + return await message.reply('❌ Нельзя отправить пустой текст!', quote=True) + + # Отправляем текст + await message.reply(text) + + +@PyAr.command( + description='Вывод информации о аккаунте', + usage='/info', + example='/info', +) +async def info(message: PyroArgs.types.Message): + # Отправляем информацию о пользователе + await message.reply( + f'👤 Имя: `{message.from_user.first_name}`\n' + f'🆔 ID: `{message.from_user.id}`' + ) + + +# Команда для фейкового бана +@PyAr.command( + description='Фейковый бан', + usage='/ban [пользователь] (время) (причина)', + example='/ban @user 200 Спам', + permissions_level=1 +) +async def ban( + message: PyroArgs.types.Message, + user: str, + ban_time: int = 120, + *, + reason: str +): + # Фейковый бан + await message.reply(f'Пользователь `{user}` был забанен на `{ban_time}` секунд по причине: `{reason}`.') + + +# Команда для вызова ошибки +@PyAr.command( + description='Вызывает исключение', + usage='/error', + example='/error', +) +async def error(message: PyroArgs.types.Message): + print(1 / 0) # ВНИМАНИЕ! Это вызовет исключение "ZeroDivisionError" для теста + + +# Запуск клиента +app.run() diff --git a/examples/permissions_example.py b/examples/permissions_example.py deleted file mode 100644 index 3c52575..0000000 --- a/examples/permissions_example.py +++ /dev/null @@ -1,33 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("permissions_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Permissions checker function -@PyAr.permissions_checker -async def check(message: types.Message, required_permission: int): - current_permission = 0 - - if message.from_user.id in [123456789, 987654321]: # Admins users ids list - current_permission = 999 - - return current_permission >= required_permission - - -# Command 'admin' with permissions -@PyAr.command(permissions_level=999) -async def admin(message: types.Message): - await message.reply("You are an admin!") - - -# Command 'user' with permissions -@PyAr.command() # default permissions_level is 0 -async def user(message: types.Message): - await message.reply("You are a user!") - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/examples/say_command.py b/examples/say_command.py deleted file mode 100644 index a6c4d86..0000000 --- a/examples/say_command.py +++ /dev/null @@ -1,17 +0,0 @@ -from pyrogram import Client -from PyroArgs import PyroArgs, types - -# Initialize client and PyroArgs -app = Client("auto_type_example", api_id=12345, api_hash="abcdef") -PyAr = PyroArgs(app, ['/', '!']) - - -# Command 'say' with auto typed arguments -@PyAr.command() -async def say(message: types.Message, *, text: str): - await message.reply(text, quote=True) - - -# Run the bot -if __name__ == "__main__": - app.run() diff --git a/setup.py b/setup.py index aeb5ea1..e800918 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,9 @@ # setup.py -from setuptools import setup, find_packages - +from setuptools import find_packages, setup setup( name='PyroArgs', - version='1.3', # ВЕРСИЯ + version='1.4', # ВЕРСИЯ description='Удобная обработка аргументов команд для Pyrogram', long_description=open('README.md', encoding='utf-8').read(), long_description_content_type='text/markdown', @@ -14,8 +13,11 @@ packages=find_packages(), install_requires=[ 'pyrogram>=2.0.0', - 'TgCrypto>=1.2.2', ], + extras_require={ + 'all': ['TgCrypto>=1.2.2'], + 'crypto': ['TgCrypto>=1.2.2'], + }, classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', diff --git a/tests/test_errors/test_ArgumentTypeError.py b/tests/test_errors/test_ArgumentTypeError.py deleted file mode 100644 index ebea3a2..0000000 --- a/tests/test_errors/test_ArgumentTypeError.py +++ /dev/null @@ -1,30 +0,0 @@ -# tests/test_errors/test_ArgumentTypeError.py -import unittest -from PyroArgs.errors import ArgumentTypeError -from PyroArgs.types import Message - - -class TestArgumentTypeError(unittest.TestCase): - - def test_argument_type_error(self): - error = ArgumentTypeError( - name='test_command', - message_object=Message(), - parsed_args=['abc'], - parsed_kwargs={}, - errored_arg_name='arg1', - errored_arg_position=1, - required_type=int - ) - expected_message = ( - 'ArgumentTypeError: Argument "arg1" at position 1 cannot convert to required type "int".' - ) - self.assertEqual(error.name, 'test_command') - self.assertEqual(error.errored_arg_name, 'arg1') - self.assertEqual(error.errored_arg_position, 1) - self.assertEqual(error.required_type, int) - self.assertEqual(str(error), expected_message) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_errors/test_ArgumentsError.py b/tests/test_errors/test_ArgumentsError.py deleted file mode 100644 index d1c64a0..0000000 --- a/tests/test_errors/test_ArgumentsError.py +++ /dev/null @@ -1,24 +0,0 @@ -# tests/test_errors/test_ArgumentsError.py -import unittest -from PyroArgs.errors import ArgumentsError -from PyroArgs.types import Message - - -class TestArgumentsError(unittest.TestCase): - - def test_arguments_error(self): - error = ArgumentsError( - name='test_command', - message_object=Message(), - parsed_args=[1, 2], - parsed_kwargs={'a': 3}, - error_text='An error occurred.' - ) - self.assertEqual(error.name, 'test_command') - self.assertEqual(error.parsed_args, [1, 2]) - self.assertEqual(error.parsed_kwargs, {'a': 3}) - self.assertEqual(str(error), 'An error occurred.') - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_errors/test_CommandError.py b/tests/test_errors/test_CommandError.py deleted file mode 100644 index a99c3d8..0000000 --- a/tests/test_errors/test_CommandError.py +++ /dev/null @@ -1,27 +0,0 @@ -# tests/test_errors/test_CommandError.py -import unittest -from PyroArgs.errors import CommandError -from PyroArgs.types import Message - - -class TestCommandError(unittest.TestCase): - - def test_command_error(self): - original_exception = ValueError('Invalid value') - error = CommandError( - name='test_command', - message=Message(), - parsed_args=[1, 2], - parsed_kwargs={'a': 3}, - error_message='An error occurred.', - original_error=original_exception - ) - expected_message = 'Command error: Error in command "test_command".' - self.assertEqual(error.name, 'test_command') - self.assertEqual(error.error_message, 'An error occurred.') - self.assertEqual(error.original_error, original_exception) - self.assertEqual(str(error), expected_message) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_errors/test_CommandPermissionError.py b/tests/test_errors/test_CommandPermissionError.py deleted file mode 100644 index 12cba44..0000000 --- a/tests/test_errors/test_CommandPermissionError.py +++ /dev/null @@ -1,24 +0,0 @@ -# tests/test_errors/test_CommandPermissionError.py -import unittest -from PyroArgs.errors import CommandPermissionError -from PyroArgs.types import Message - - -class TestCommandPermissionError(unittest.TestCase): - - def test_command_permission_error(self): - error = CommandPermissionError( - name='test_command', - message=Message(), - permission_level=2 - ) - expected_message = ( - 'Permissions error: User does not have permission to use command "test_command".' - ) - self.assertEqual(error.name, 'test_command') - self.assertEqual(error.permission_level, 2) - self.assertEqual(str(error), expected_message) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_errors/test_MissingArgumentError.py b/tests/test_errors/test_MissingArgumentError.py deleted file mode 100644 index 7960138..0000000 --- a/tests/test_errors/test_MissingArgumentError.py +++ /dev/null @@ -1,28 +0,0 @@ -# tests/test_errors/test_MissingArgumentError.py -import unittest -from PyroArgs.errors import MissingArgumentError -from PyroArgs.types import Message - - -class TestMissingArgumentError(unittest.TestCase): - - def test_missing_argument_error(self): - error = MissingArgumentError( - name='test_command', - message_object=Message(), - parsed_args=[1], - parsed_kwargs={}, - missing_arg_name='arg2', - missing_arg_position=2 - ) - expected_message = ( - 'MissingArgumentError: Missing required argument "arg2" at position 2.' - ) - self.assertEqual(error.name, 'test_command') - self.assertEqual(error.missing_arg_name, 'arg2') - self.assertEqual(error.missing_arg_position, 2) - self.assertEqual(str(error), expected_message) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_parser.py b/tests/test_parser.py deleted file mode 100644 index 1fed7e3..0000000 --- a/tests/test_parser.py +++ /dev/null @@ -1,102 +0,0 @@ -# tests/test_parser.py -import unittest -from PyroArgs.parser import get_command_and_args, parse_command -from PyroArgs.errors import MissingArgumentError, ArgumentTypeError - - -class TestParser(unittest.TestCase): - - def test_get_command_and_args(self): - text = '/start arg1 arg2' - prefixes = ['/'] - cmd, args = get_command_and_args(text, prefixes) - self.assertEqual(cmd, 'start') - self.assertEqual(args, 'arg1 arg2') - - def test_get_command_and_args_no_prefix(self): - text = 'start arg1 arg2' - prefixes = ['/'] - with self.assertRaises(NameError): - get_command_and_args(text, prefixes) - - def test_parse_command_with_correct_args(self): - def func(a: int, b: str): - return a, b - - command = '123 hello' - result_args, result_kwargs = parse_command(func, command) - self.assertEqual(result_args, [123, 'hello']) - self.assertEqual(result_kwargs, {}) - - def test_parse_command_missing_argument(self): - def func(a: int, b: str): - return a, b - - command = '123' - with self.assertRaises(MissingArgumentError): - parse_command(func, command) - - def test_parse_command_type_error(self): - def func(a: int, b: str): - return a, b - - command = 'hello world' - with self.assertRaises(ArgumentTypeError): - parse_command(func, command) - - def test_parse_command_with_default(self): - def func(a: int, b: str = 'default'): - return a, b - - command = '123' - result_args, result_kwargs = parse_command(func, command) - self.assertEqual(result_args, [123, 'default']) - self.assertEqual(result_kwargs, {}) - - def test_parse_command_keyword_only(self): - def func(a: int, *, b: str): - return a, b - - command = '123 hello world' - result_args, result_kwargs = parse_command(func, command) - self.assertEqual(result_args, [123]) - self.assertEqual(result_kwargs, {'b': 'hello world'}) - - def test_parse_command_var_positional(self): - def func(a: int, *args): - return a, args - - command = '123 456 789' - with self.assertRaises(SyntaxError): - parse_command(func, command) - - def test_parse_command_var_keyword(self): - def func(a: int, **kwargs): - return a, kwargs - - command = '123 key=value' - with self.assertRaises(SyntaxError): - parse_command(func, command) - - def test_parse_command_bool(self): - def func(a: bool): - return a - - command_true = 'true' - command_false = 'false' - result_args_true, _ = parse_command(func, command_true) - result_args_false, _ = parse_command(func, command_false) - self.assertTrue(result_args_true[0]) - self.assertFalse(result_args_false[0]) - - def test_get_bool_invalid_value(self): - def func(a: bool): - return a - - command = 'maybe' - with self.assertRaises(ValueError): - parse_command(func, command) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_pyroargs.py b/tests/test_pyroargs.py deleted file mode 100644 index 170a423..0000000 --- a/tests/test_pyroargs.py +++ /dev/null @@ -1,51 +0,0 @@ -# tests/test_pyroargs.py -import unittest -from pyrogram import Client -from PyroArgs.pyroargs import PyroArgs -from PyroArgs.types.message import Message -from PyroArgs.errors import CommandPermissionError -from unittest.mock import MagicMock - - -class TestPyroArgs(unittest.TestCase): - - def setUp(self): - self.client = Client('test_bot') - self.pyroargs = PyroArgs(self.client) - - def test_command_decorator(self): - @self.pyroargs.command(name='test') - async def test_command(message, arg1): - return arg1 - - self.assertIn( - 'test', self.pyroargs.registry.commands['General'][0].command) - - def test_permission_checker(self): - called = [] - - @self.pyroargs.permissions_checker - async def checker(message, level): - called.append((message, level)) - return False - - @self.pyroargs.command(name='admin', permissions_level=2) - async def admin_command(message): - pass - - message = Message() - message.text = '/admin' - message.from_user = MagicMock(id=123) - - handler = self.pyroargs.bot.handlers[0][0].callback - - with self.assertRaises(CommandPermissionError): - import asyncio - asyncio.run(handler(self.client, message)) - - self.assertEqual(len(called), 1) - self.assertEqual(called[0][1], 2) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_types/test_command.py b/tests/test_types/test_command.py deleted file mode 100644 index 0ddc0f3..0000000 --- a/tests/test_types/test_command.py +++ /dev/null @@ -1,40 +0,0 @@ -# tests/test_types/test_command.py -import unittest -from PyroArgs.types.command import Command - - -class TestCommand(unittest.TestCase): - - def test_command_creation(self): - cmd = Command( - command='start', - description='Start the bot', - usage='/start', - example='/start', - permissions=0, - aliases=['run'], - command_meta_data={'key': 'value'} - ) - self.assertEqual(cmd.command, 'start') - self.assertEqual(cmd.description, 'Start the bot') - self.assertEqual(cmd.usage, '/start') - self.assertEqual(cmd.example, '/start') - self.assertEqual(cmd.permissions, 0) - self.assertEqual(cmd.aliases, ['run']) - self.assertEqual(cmd.command_meta_data, {'key': 'value'}) - - def test_has_permission(self): - cmd = Command( - command='admin', - description='Admin command', - usage='/admin', - example='/admin', - permissions=2 - ) - self.assertTrue(cmd.has_permission(2)) - self.assertTrue(cmd.has_permission(3)) - self.assertFalse(cmd.has_permission(1)) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_types/test_commandRegistry.py b/tests/test_types/test_commandRegistry.py deleted file mode 100644 index 0cdb720..0000000 --- a/tests/test_types/test_commandRegistry.py +++ /dev/null @@ -1,61 +0,0 @@ -# tests/test_types/test_commandRegistry.py -import unittest -from PyroArgs.types.commandRegistry import CommandRegistry -from PyroArgs.types.command import Command - - -class TestCommandRegistry(unittest.TestCase): - - def setUp(self): - self.registry = CommandRegistry() - self.command1 = Command( - command='start', - description='Start the bot', - usage='/start', - example='/start', - permissions=0 - ) - self.command2 = Command( - command='help', - description='Help command', - usage='/help', - example='/help', - permissions=0, - aliases=['h'] - ) - self.registry.add_command(self.command1, 'General') - self.registry.add_command(self.command2, 'General') - - def test_add_command(self): - self.assertIn('General', self.registry.commands) - self.assertEqual(len(self.registry.commands['General']), 2) - - def test_get_commands_by_category(self): - cmds = self.registry.get_commands_by_category('General') - self.assertEqual(cmds, [self.command1, self.command2]) - - def test_find_command(self): - cmd = self.registry.find_command('start') - self.assertEqual(cmd, self.command1) - cmd_alias = self.registry.find_command('h') - self.assertEqual(cmd_alias, self.command2) - cmd_none = self.registry.find_command('nonexistent') - self.assertIsNone(cmd_none) - - def test_iterate_categories(self): - categories = list(self.registry.iterate_categories()) - self.assertEqual(categories, ['General']) - - def test_iterate_commands(self): - cmds = list(self.registry.iterate_commands()) - self.assertEqual(cmds, [self.command1, self.command2]) - - def test_iterate_categories_with_commands(self): - categories_with_cmds = list( - self.registry.iterate_categories_with_commands()) - self.assertEqual(categories_with_cmds, [ - ('General', [self.command1, self.command2])]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_types/test_events.py b/tests/test_types/test_events.py deleted file mode 100644 index 837c748..0000000 --- a/tests/test_types/test_events.py +++ /dev/null @@ -1,40 +0,0 @@ -# tests/test_types/test_events.py -import unittest -from PyroArgs.types.events import Events -from PyroArgs.types.message import Message - - -class TestEvents(unittest.TestCase): - - def setUp(self): - self.events = Events() - self.message = Message() - - def test_on_before_use_command(self): - called = [] - - @self.events.on_before_use_command - async def handler(message, command, args, kwargs): - called.append((message, command, args, kwargs)) - - self.assertEqual(len(self.events._on_before_use_command_handlers), 1) - self.assertIs(self.events._on_before_use_command_handlers[0], handler) - - # Similar tests can be written for other event handlers - - def test_trigger_before_use_command(self): - called = [] - - @self.events.on_before_use_command - async def handler(message, command, args, kwargs): - called.append((message, command, args, kwargs)) - - import asyncio - asyncio.run(self.events._trigger_before_use_command( - self.message, 'test', [1], {'a': 2})) - self.assertEqual(len(called), 1) - self.assertEqual(called[0], (self.message, 'test', [1], {'a': 2})) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_types/test_logger.py b/tests/test_types/test_logger.py deleted file mode 100644 index e69de29..0000000