diff options
| author | 2018-04-29 20:25:16 +0200 | |
|---|---|---|
| committer | 2018-04-29 20:25:16 +0200 | |
| commit | 945e3985a473d2154043943d71f8f3dac4e6e29f (patch) | |
| tree | bc67d97d1bc473dbf42085f0aeabc3fac1389e5a | |
| parent | use pip script (diff) | |
Python parser cleanup (#62)
* Cleaned up the python parser by abstracting a little, and added a few channels as exceptions to the tags cooldown system
* Refactored to make the code more DRY
* Addressing Volcyy's requests for change
| -rw-r--r-- | bot/__init__.py | 253 | ||||
| -rw-r--r-- | bot/cogs/tags.py | 21 | ||||
| -rw-r--r-- | bot/constants.py | 2 | 
3 files changed, 152 insertions, 124 deletions
| diff --git a/bot/__init__.py b/bot/__init__.py index 02b3d5e98..429c01829 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -83,63 +83,105 @@ def _get_word(self) -> str:      the bot command part of a message, but      allows the command to ignore case sensitivity,      and allows commands to have Python syntax. - -    Example of valid Python syntax calls: -    ------------------------------ -    bot.tags.set("test", 'a dark, dark night') -    bot.help(tags.delete) -    bot.hELP(tags.delete)      """ -    pos = 0 -    while not self.eof: -        try: -            current = self.buffer[self.index + pos] -            if current.isspace() or current == "(" or current == "[": -                break -            pos += 1 -        except IndexError: -            break +    def parse_python(buffer_pos): +        """ +        Takes the instance of the view and parses the buffer, if it contains valid python syntax. +        This may fail spectacularly with a SyntaxError, which must be caught by the caller. -    self.previous = self.index -    result = self.buffer[self.index:self.index + pos] -    self.index += pos -    next = None +        Example of valid Python syntax calls: +        ------------------------------ +        bot.tags.set("test", 'a dark, dark night') +        bot.help(tags.delete) +        bot.hELP(tags.delete) +        bot.tags['internet'] +        bot.tags['internet'] = "A series of tubes" -    # Check what's after the '(' -    if len(self.buffer) != self.index: -        next = self.buffer[self.index + 1] +        :return: the parsed command +        """ + +        # Check what's after the '(' or '[' +        next_char = None +        if len(self.buffer) != self.index: +            next_char = self.buffer[self.index + 1] -    # Is it possible to parse this without syntax error? -    syntax_valid = True -    try:          # Catch raw channel, member or role mentions and wrap them in quotes. +        tempbuffer = self.buffer          tempbuffer = re.sub(r"(<(?:@|@!|[#&])\d+>)",                              r'"\1"', -                            self.buffer) -        ast.literal_eval(tempbuffer[self.index:]) -        self.buffer = tempbuffer -    except SyntaxError: -        log.warning("The command cannot be parsed by ast.literal_eval because it raises a SyntaxError.") -        # TODO: It would be nice if this actually made the bot return a SyntaxError. ClickUp #1b12z  # noqa: T000 -        syntax_valid = False - -    # Conditions for a valid, parsable command. -    python_parse_conditions = ( -        current == "(" -        and next -        and next != ")" -        and syntax_valid -    ) - -    if python_parse_conditions: -        log.debug(f"A python-style command was used. Attempting to parse. Buffer is {self.buffer}. " +                            tempbuffer) + +        # Let's parse! +        log.debug("A python-style command was used. Attempting to parse. " +                  f"Buffer is '{self.buffer}'. Tempbuffer is '{tempbuffer}'. "                    "A step-by-step can be found in the trace log.") -        # Parse the args -        log.trace("Parsing command with ast.literal_eval.") -        args = self.buffer[self.index:] -        args = ast.literal_eval(args) +        if current == "(" and next_char == ")": +            # Move the cursor to capture the ()'s +            log.debug("User called command without providing arguments.") +            buffer_pos += 2 +            parsed_result = self.buffer[self.previous:self.index + (buffer_pos+2)] +            self.index += 2 +            args = '' + +        if current == "(" and next_char: + +            # Parse the args +            log.trace(f"Parsing command with ast.literal_eval. args are {tempbuffer[self.index:]}") +            args = tempbuffer[self.index:] +            args = ast.literal_eval(args) + +            # Return what we'd return for a non-python syntax call +            log.trace(f"Returning {self.buffer[self.previous:self.index]}") +            parsed_result = self.buffer[self.previous:self.index] + +        # Check if a command in the form of `bot.tags['ask']` +        # or alternatively `bot.tags['ask'] = 'whatever'` was used. +        elif current == "[": + +            # Syntax is `bot.tags['ask']` => mimic `getattr` +            log.trace(f"Got a command candidate for getitem / setitem parsing: {self.buffer}") +            if self.buffer.endswith("]"): + +                # Key: The first argument, specified `bot.tags[here]` +                key = tempbuffer[self.index + 1:tempbuffer.rfind("]")] +                log.trace(f"Command mimicks getitem. Key: {key!r}") +                args = ast.literal_eval(key) + +                # Use the cogs `.get` method. +                parsed_result = self.buffer[self.previous:self.index] + ".get" + +            # Syntax is `bot.tags['ask'] = 'whatever'` => mimic `setattr` +            elif "=" in self.buffer and not self.buffer.endswith("="): +                equals_pos = tempbuffer.find("=") +                closing_bracket_pos = tempbuffer.rfind("]", 0, equals_pos) + +                # Key: The first argument, specified `bot.tags[here]` +                key_contents = tempbuffer[self.index + 1:closing_bracket_pos] +                key = ast.literal_eval(key_contents) + +                # Value: The second argument, specified after the `=` +                right_hand = tempbuffer.split("=", maxsplit=1)[1].strip() +                value = ast.literal_eval(right_hand) + +                # If the value is a falsy value - mimick `bot.tags.delete(key)` +                if not value: +                    log.trace(f"Command mimicks delitem. Key: {key!r}.") +                    parsed_result = self.buffer[self.previous:self.index] + ".delete" +                    args = key + +                # Otherwise, assume assignment, for example `bot.tags['this'] = 'that'` +                else: +                    log.trace(f"Command mimicks setitem. Key: {key!r}, value: {value!r}.") +                    parsed_result = self.buffer[self.previous:self.index] + ".set" +                    args = (key, value) + +            # Syntax is god knows what, pass it along +            else: +                parsed_result = self.buffer +                args = '' +                log.trace(f"Command is of unknown syntax: {self.buffer}")          # Force args into container          if not isinstance(args, tuple): @@ -158,82 +200,57 @@ def _get_word(self) -> str:              arg = arg.replace('"', '\\"')              # Adding double quotes to every argument -            log.trace(f"Wrapping all args in double quotes.") +            log.trace("Wrapping all args in double quotes.")              new_args.append(f'"{arg}"') -        # Add the result to the buffer -        new_args = " ".join(new_args) -        self.buffer = f"{self.buffer[:self.index]} {new_args}" -        log.trace(f"Modified the buffer. New buffer is now {self.buffer}") - -        # Recalibrate the end since we've removed commas +        # Reconstruct valid discord.py syntax +        prefix = self.buffer[:self.previous] +        self.buffer = f"{prefix}{parsed_result} {' '.join(new_args)}" +        self.index = len(f"{prefix}{parsed_result}")          self.end = len(self.buffer) +        log.trace(f"Modified the buffer. New buffer is now '{self.buffer}'") -    elif current == "(" and next == ")": -        # Move the cursor to capture the ()'s -        log.debug("User called command without providing arguments.") -        pos += 2 -        result = self.buffer[self.previous:self.index + (pos+2)] -        self.index += 2 - -    # Check if a command in the form of `bot.tags['ask']` -    # or alternatively `bot.tags['ask'] = 'whatever'` was used. -    elif current == "[": -        log.trace(f"Got a command candidate for getitem / setitem mimick: {self.buffer}") -        # Syntax is `bot.tags['ask']` => mimic `getattr` -        if self.buffer.endswith("]"): -            # Key: The first argument, specified `bot.tags[here]` -            key = self.buffer[self.index + 1:self.buffer.rfind("]")] -            log.trace(f"Command mimicks getitem. Key: {key!r}") - -            # note: if not key, this corresponds to an empty argument -            #       so this should throw / return a SyntaxError ? -            args = ast.literal_eval(key) - -            # Use the cog's `get` command. -            result = self.buffer[self.previous:self.index] + ".get" - -        # Syntax is `bot.tags['ask'] = 'whatever'` => mimic `setattr` -        elif "=" in self.buffer and not self.buffer.endswith("="): -            equals_pos = self.buffer.find("=") -            closing_bracket_pos = self.buffer.rfind("]", 0, equals_pos) - -            # Key: The first argument, specified `bot.tags[here]` -            key_contents = self.buffer[self.index + 1:closing_bracket_pos] -            key = ast.literal_eval(key_contents) - -            # Value: The second argument, specified after the `=` -            right_hand = self.buffer.split("=", maxsplit=1)[1].strip() -            value = ast.literal_eval(right_hand) - -            # If the value is a falsy value - mimick `bot.tags.delete(key)` -            if not value: -                log.trace(f"Command mimicks delitem. Key: {key!r}.") -                result = self.buffer[self.previous:self.index] + ".delete" -                args = key - -            # Otherwise, assume assignment, for example `bot.tags['this'] = 'that'` -            else: -                # Allow using double quotes in triple double quote string -                value = value.replace('"', '\\"') -                log.trace(f"Command mimicks setitem. Key: {key!r}, value: {value!r}.") - -                # Use the cog's `set` command. -                result = self.buffer[self.previous:self.index] + ".set" -                args = f'"{key}" "{value}"' - -        # Syntax is god knows what, pass it along -        # in the future, this should probably return / throw SyntaxError -        else: -            result = self.buffer -            args = '' -            log.trace(f"Command is of unknown syntax: {self.buffer}") +        return parsed_result -        # Reconstruct valid discord.py syntax -        self.buffer = f"{result} {args}" -        self.index = len(result) -        self.end = len(self.buffer) -        log.trace(f"Mimicked command: {self.buffer}") +    # Iterate through the buffer and determine +    pos = 0 +    while not self.eof: +        try: +            current = self.buffer[self.index + pos] +            if current.isspace() or current == "(" or current == "[": +                break +            pos += 1 +        except IndexError: +            break + +    self.previous = self.index +    result = self.buffer[self.index:self.index + pos] +    self.index += pos + +    # If the command looks like a python syntax command, try to parse it. +    if current == "(" or current == "[": +        try: +            result = parse_python(pos) + +        except SyntaxError: +            log.debug( +                "A SyntaxError was encountered while parsing a python-syntaxed command:" +                "\nTraceback (most recent call last):\n" +                '  File "<stdin>", line 1, in <module>\n' +                f"    {self.buffer}\n" +                f"     {' ' * self.index}^\n" +                "SyntaxError: invalid syntax" +            ) +            return + +        except ValueError: +            log.debug( +                "A ValueError was encountered while parsing a python-syntaxed command:" +                "\nTraceback (most recent call last):\n" +                '  File "<stdin>", line 1, in <module>\n' +                f"ValueError: could not ast.literal_eval the following: '{self.buffer}'" +            ) +            return      return result diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 24ace01c7..1c8f54d9f 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -9,8 +9,9 @@ from discord.ext.commands import (  )  from bot.constants import ( -    ADMIN_ROLE, MODERATOR_ROLE, OWNER_ROLE, -    SITE_API_KEY, SITE_API_TAGS_URL, TAG_COOLDOWN, +    ADMIN_ROLE, BOT_COMMANDS_CHANNEL, DEVTEST_CHANNEL, +    HELPERS_CHANNEL, MODERATOR_ROLE, OWNER_ROLE, +    SITE_API_KEY, SITE_API_TAGS_URL, TAG_COOLDOWN  )  from bot.decorators import with_role  from bot.exceptions import CogBadArgument @@ -18,6 +19,12 @@ from bot.pagination import LinePaginator  log = logging.getLogger(__name__) +TEST_CHANNELS = ( +    DEVTEST_CHANNEL, +    BOT_COMMANDS_CHANNEL, +    HELPERS_CHANNEL +) +  class TagNameConverter(Converter):      @staticmethod @@ -209,10 +216,12 @@ class Tags:              if tag_name:                  log.debug(f"{ctx.author} requested the tag '{tag_name}'")                  embed.title = tag_name -                self.tag_cooldowns[tag_name] = { -                    "time": time.time(), -                    "channel": ctx.channel.id -                } + +                if ctx.channel.id not in TEST_CHANNELS: +                    self.tag_cooldowns[tag_name] = { +                        "time": time.time(), +                        "channel": ctx.channel.id +                    }              else:                  embed.title = "**Current tags**" diff --git a/bot/constants.py b/bot/constants.py index b0a2feb2f..7b2d8214a 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -5,6 +5,7 @@ PYTHON_GUILD = 267624335836053506  # Channels  BOT_CHANNEL = 267659945086812160 +BOT_COMMANDS_CHANNEL = 267659945086812160  CHECKPOINT_TEST_CHANNEL = 422077681434099723  DEVLOG_CHANNEL = 409308876241108992  DEVTEST_CHANNEL = 414574275865870337 @@ -12,6 +13,7 @@ HELP1_CHANNEL = 303906576991780866  HELP2_CHANNEL = 303906556754395136  HELP3_CHANNEL = 303906514266226689  HELP4_CHANNEL = 439702951246692352 +HELPERS_CHANNEL = 385474242440986624  MOD_LOG_CHANNEL = 282638479504965634  PYTHON_CHANNEL = 267624335836053506  VERIFICATION_CHANNEL = 352442727016693763 | 
