From b989aaeea2221f1c364bb0a9e399dfb3287f15a7 Mon Sep 17 00:00:00 2001 From: Chris W Date: Fri, 13 Oct 2023 12:34:08 -0600 Subject: [PATCH] move arg stuff to its own file --- cyber_fenneko/bot.py | 101 +--------------- cyber_fenneko/commands/highlight.py | 2 +- cyber_fenneko/commands/paste.py | 2 +- cyber_fenneko/commands/ping.py | 2 +- cyber_fenneko/internal/arg.py | 36 ------ cyber_fenneko/internal/arg_parser.py | 133 ++++++++++++++++++++++ cyber_fenneko/internal/command.py | 2 +- cyber_fenneko/internal/command_context.py | 2 +- cyber_fenneko/internal/constants.py | 2 +- cyber_fenneko/internal/utils.py | 0 10 files changed, 140 insertions(+), 142 deletions(-) delete mode 100644 cyber_fenneko/internal/arg.py create mode 100644 cyber_fenneko/internal/arg_parser.py create mode 100644 cyber_fenneko/internal/utils.py diff --git a/cyber_fenneko/bot.py b/cyber_fenneko/bot.py index 1d1769a..921c343 100644 --- a/cyber_fenneko/bot.py +++ b/cyber_fenneko/bot.py @@ -1,12 +1,10 @@ -from typing import Dict, List, Union - import logging from telethon import TelegramClient from telethon.tl.custom.message import Message from cyber_fenneko.internal.constants import GLOBAL_ARGS from cyber_fenneko.internal.command import Command from cyber_fenneko.internal.command_context import CommandContext -from cyber_fenneko.internal.arg import Arg, ArgType +from cyber_fenneko.internal.arg_parser import parse_args class Bot: def __init__(self, client: TelegramClient): @@ -90,100 +88,3 @@ class Bot: # if the command is marked as 'delete', delete the message that triggered it if ctx.args['delete']: await event.message.delete() - - def parse_args(self, text: str, cmd_args: List[Arg]) -> (Dict[str, Arg], str): - """ - Take an incoming string and parse args from it. - Args are defined one of three ways: - - name:value - - .name - shorthand for name:true - - !name - shorthand for name:false - Args are also only valid at the beginning of a message, - so as soon as we encounter a non-arg we stop parsing. - """ - args: Dict[str, Arg] = { arg.name: arg for arg in cmd_args } - aliases: Dict[str, str] = { alias: arg.name for arg in cmd_args for alias in arg.aliases } - - get_arg = lambda name: args.get(name) or args.get(aliases.get(name)) - - parsed_args: Dict[str, Union[str, int, bool]] = {} - tokens = text.split() - while tokens: - token = tokens.pop(0) - - if token.startswith('.'): - # shorthand for name:true - arg = get_arg(token[1:]) - if arg: - if args[arg.name].type == ArgType.bool: - parsed_args[arg.name] = True - else: - raise ValueError(f'Arg "{arg.name}" is not a boolean, but a boolean was provided') - else: - self.logger.warning(f'Unknown arg "{arg.name}"') - continue - elif token.startswith('!'): - # shorthand for name:false - arg = get_arg(token[1:]) - if arg: - if arg.type == ArgType.bool: - parsed_args[arg.name] = False - else: - raise ValueError(f'Arg "{arg.name}" is not a boolean, but a boolean was provided') - else: - self.logger.warning(f'Unknown arg "{arg.name}"') - continue - elif ':' in token: - # name:value - # value might be a quoted string, so we need to handle that - arg_name, arg_value = token.split(':', 1) - arg = get_arg(arg_name) - if arg: - if arg.type == ArgType.str: - # we need to check if the value starts with a quote, - # if so we need to loop through tokens until we find the end quote - if arg_value.startswith('"'): - while not arg_value.endswith('"') and tokens: - arg_value += ' ' + tokens.pop(0) - if not arg_value.endswith('"'): - raise ValueError(f'Unterminated string for arg "{arg.name}"') - arg_value = arg_value[1:-1] - parsed_args[arg.name] = arg_value - elif arg.type == ArgType.bool: - # if the arg is a boolean, we need to check if the value is true or false - if arg_value.lower() == 'true': - parsed_args[arg.name] = True - elif arg_value.lower() == 'false': - parsed_args[arg.name] = False - else: - raise ValueError(f'Invalid boolean value {arg_value} for arg "{arg.name}"') - elif arg.type == ArgType.int: - # if the arg is an int, we need to check if the value is an int - try: - parsed_args[arg.name] = int(arg_value) - except ValueError: - raise ValueError(f'Invalid int value {arg_value} for arg "{arg.name}"') - else: - raise ValueError(f'Invalid arg type {arg.type} for arg "{arg.name}"') - else: - self.logger.warning(f'Unknown arg "{arg.name}"') - continue - - # If we get here, we've encountered a non-arg token - # so we stop parsing - break - - # Check if any required args are missing - missing_args = [arg.name for arg in cmd_args if arg.required and arg.name not in parsed_args] - if missing_args: - raise ValueError(f'Missing required args: {", ".join(missing_args)}') - - # remove the parsed args from the text - text = ' '.join(tokens[len(parsed_args):]) - - # add the default values for any args that weren't specified - for arg in args.values(): - if arg.name not in parsed_args: - parsed_args[arg.name] = arg.default - - return parsed_args, text \ No newline at end of file diff --git a/cyber_fenneko/commands/highlight.py b/cyber_fenneko/commands/highlight.py index 99ee8d8..fd307c6 100644 --- a/cyber_fenneko/commands/highlight.py +++ b/cyber_fenneko/commands/highlight.py @@ -6,7 +6,7 @@ from collections import OrderedDict from telethon.tl.types import TypeMessageEntity, MessageEntityPre from .. import bot from ..internal.command_context import CommandContext -from ..internal.arg import Arg, ArgType +from ..internal.arg_parser import Arg, ArgType ENDPOINT_URL = 'https://inkify.0x45.st' diff --git a/cyber_fenneko/commands/paste.py b/cyber_fenneko/commands/paste.py index 9f8eae8..f0da1c3 100644 --- a/cyber_fenneko/commands/paste.py +++ b/cyber_fenneko/commands/paste.py @@ -4,7 +4,7 @@ from telethon.tl.types import MessageEntityPre from telethon.tl.custom.message import Message from .. import bot from ..internal.command_context import CommandContext -from ..internal.arg import Arg, ArgType +from ..internal.arg_parser import Arg, ArgType ENDPOINT = 'https://0x45.st/api/pastes' diff --git a/cyber_fenneko/commands/ping.py b/cyber_fenneko/commands/ping.py index e36a62f..6fc3161 100644 --- a/cyber_fenneko/commands/ping.py +++ b/cyber_fenneko/commands/ping.py @@ -1,7 +1,7 @@ import time from .. import bot from ..internal.command_context import CommandContext -from ..internal.arg import Arg, ArgType +from ..internal.arg_parser import Arg, ArgType @bot.command( 'ping', diff --git a/cyber_fenneko/internal/arg.py b/cyber_fenneko/internal/arg.py deleted file mode 100644 index aa6e6d4..0000000 --- a/cyber_fenneko/internal/arg.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum -from typing import List - - -ArgType = Enum('ArgType', ['str', 'int', 'bool', 'peer_id']) - -class Arg: - def __init__( - self, - name: str, - *, - # The type of this argument - type: ArgType = ArgType.str, - - # Aliases for this argument - aliases: List[str] = [], - - # Is this argument required? - required: bool = False, - - # The default value for this argument - default: any = None, - - # The description for this argument, displayed in the help command - description: str = None, - - # Special designation for global args - global_arg: bool = False, - ): - self.name = name - self.type = type - self.aliases = aliases - self.required = required - self.default = default - self.description = description - self.global_arg = global_arg \ No newline at end of file diff --git a/cyber_fenneko/internal/arg_parser.py b/cyber_fenneko/internal/arg_parser.py new file mode 100644 index 0000000..4c16bbd --- /dev/null +++ b/cyber_fenneko/internal/arg_parser.py @@ -0,0 +1,133 @@ +from enum import Enum +from typing import Dict, List, Union + + +ArgType = Enum('ArgType', ['str', 'int', 'bool', 'peer_id']) + +class Arg: + def __init__( + self, + name: str, + *, + # The type of this argument + type: ArgType = ArgType.str, + + # Aliases for this argument + aliases: List[str] = [], + + # Is this argument required? + required: bool = False, + + # The default value for this argument + default: any = None, + + # The description for this argument, displayed in the help command + description: str = None, + + # Special designation for global args + global_arg: bool = False, + ): + self.name = name + self.type = type + self.aliases = aliases + self.required = required + self.default = default + self.description = description + self.global_arg = global_arg + +def parse_args(self, text: str, cmd_args: List[Arg]) -> (Dict[str, Arg], str): + """ + Take an incoming string and parse args from it. + Args are defined one of three ways: + - name:value + - .name - shorthand for name:true + - !name - shorthand for name:false + Args are also only valid at the beginning of a message, + so as soon as we encounter a non-arg we stop parsing. + """ + args: Dict[str, Arg] = { arg.name: arg for arg in cmd_args } + aliases: Dict[str, str] = { alias: arg.name for arg in cmd_args for alias in arg.aliases } + + get_arg = lambda name: args.get(name) or args.get(aliases.get(name)) + + parsed_args: Dict[str, Union[str, int, bool]] = {} + tokens = text.split() + while tokens: + token = tokens.pop(0) + + if token.startswith('.'): + # shorthand for name:true + arg = get_arg(token[1:]) + if arg: + if args[arg.name].type == ArgType.bool: + parsed_args[arg.name] = True + else: + raise ValueError(f'Arg "{arg.name}" is not a boolean, but a boolean was provided') + else: + self.logger.warning(f'Unknown arg "{arg.name}"') + continue + elif token.startswith('!'): + # shorthand for name:false + arg = get_arg(token[1:]) + if arg: + if arg.type == ArgType.bool: + parsed_args[arg.name] = False + else: + raise ValueError(f'Arg "{arg.name}" is not a boolean, but a boolean was provided') + else: + self.logger.warning(f'Unknown arg "{arg.name}"') + continue + elif ':' in token: + # name:value + # value might be a quoted string, so we need to handle that + arg_name, arg_value = token.split(':', 1) + arg = get_arg(arg_name) + if arg: + if arg.type == ArgType.str: + # we need to check if the value starts with a quote, + # if so we need to loop through tokens until we find the end quote + if arg_value.startswith('"'): + while not arg_value.endswith('"') and tokens: + arg_value += ' ' + tokens.pop(0) + if not arg_value.endswith('"'): + raise ValueError(f'Unterminated string for arg "{arg.name}"') + arg_value = arg_value[1:-1] + parsed_args[arg.name] = arg_value + elif arg.type == ArgType.bool: + # if the arg is a boolean, we need to check if the value is true or false + if arg_value.lower() == 'true': + parsed_args[arg.name] = True + elif arg_value.lower() == 'false': + parsed_args[arg.name] = False + else: + raise ValueError(f'Invalid boolean value {arg_value} for arg "{arg.name}"') + elif arg.type == ArgType.int: + # if the arg is an int, we need to check if the value is an int + try: + parsed_args[arg.name] = int(arg_value) + except ValueError: + raise ValueError(f'Invalid int value {arg_value} for arg "{arg.name}"') + else: + raise ValueError(f'Invalid arg type {arg.type} for arg "{arg.name}"') + else: + self.logger.warning(f'Unknown arg "{arg.name}"') + continue + + # If we get here, we've encountered a non-arg token + # so we stop parsing + break + + # Check if any required args are missing + missing_args = [arg.name for arg in cmd_args if arg.required and arg.name not in parsed_args] + if missing_args: + raise ValueError(f'Missing required args: {", ".join(missing_args)}') + + # remove the parsed args from the text + text = ' '.join(tokens[len(parsed_args):]) + + # add the default values for any args that weren't specified + for arg in args.values(): + if arg.name not in parsed_args: + parsed_args[arg.name] = arg.default + + return parsed_args, text \ No newline at end of file diff --git a/cyber_fenneko/internal/command.py b/cyber_fenneko/internal/command.py index 61f7218..8ca9318 100644 --- a/cyber_fenneko/internal/command.py +++ b/cyber_fenneko/internal/command.py @@ -1,7 +1,7 @@ import itertools from typing import List -from .arg import Arg +from .arg_parser import Arg from ..bot import GLOBAL_ARGS class Command: diff --git a/cyber_fenneko/internal/command_context.py b/cyber_fenneko/internal/command_context.py index bff122b..3ecc8e8 100644 --- a/cyber_fenneko/internal/command_context.py +++ b/cyber_fenneko/internal/command_context.py @@ -4,7 +4,7 @@ from telethon.events import NewMessage from telethon.tl.custom.message import Message from .. import bot -from .arg import Arg +from .arg_parser import Arg class CommandContext: diff --git a/cyber_fenneko/internal/constants.py b/cyber_fenneko/internal/constants.py index cfa07d6..34fceef 100644 --- a/cyber_fenneko/internal/constants.py +++ b/cyber_fenneko/internal/constants.py @@ -1,4 +1,4 @@ -from .arg import Arg, ArgType +from .arg_parser import Arg, ArgType # Args that exist on every command GLOBAL_ARGS = [ diff --git a/cyber_fenneko/internal/utils.py b/cyber_fenneko/internal/utils.py new file mode 100644 index 0000000..e69de29