Commit 74c375e4 authored by Jeremy Pallats's avatar Jeremy Pallats 💬
Browse files

Overhaul the core exception system.

- Override __str__ instead of adding reply().
- Improve message for NoMatch and MoreThanOneMatch
- Simplify fuzzy_find and underlying substr_ind.
- Remove circular import between exc and util, extracted to separate
  file.
parent dfe7acfb
Loading
Loading
Loading
Loading
Loading
+4 −6
Original line number Diff line number Diff line
@@ -778,7 +778,7 @@ If we should contact Gears or Sidewinder""".format(system_name)
        except AttributeError as exc:
            raise cog.exc.InvalidCommandArgs("Bad subcommand of `!bgs`, see `!bgs -h` for help.") from exc
        except (cog.exc.NoMoreTargets, cog.exc.RemoteError) as exc:
            response = exc.reply()
            response = str(exc)


class Dist(Action):
@@ -1140,7 +1140,6 @@ class Hold(Action):
            response += '\n\n**{}** Have a :skull: for completing {}. Don\'t forget to redeem.'.format(
                self.duser.display_name, system.name)


        return ([hold], response)

    @check_mentions
@@ -1592,7 +1591,7 @@ class Time(Action):
                tick = await self.bot.loop.run_in_executor(
                    None, cogdb.side.next_bgs_tick, side_session, now)
        except (cog.exc.NoMoreTargets, cog.exc.RemoteError) as exc:
            tick = exc.reply()
            tick = str(exc)
        lines = [
            'Game Time: **{}**'.format(now.strftime('%H:%M:%S')),
            tick,
@@ -1829,7 +1828,7 @@ class Snipe(UM):
                new_page = self.args.cycle

                try:
                    await SCANNERS[scanner_name].asheet.change_worksheet(new_page)
                    await cogdb.scanners.SCANNERS[scanner_name].asheet.change_worksheet(new_page)
                except gspread.exceptions.WorksheetNotFound as exc:
                    msg = f"Missing **{new_page}** worksheet on {scanner_name}. Please fix and rerun cycle. No change made."
                    raise cog.exc.InvalidCommandArgs(msg) from exc
@@ -2018,7 +2017,6 @@ Date (UTC): {now}
            await self.msg.channel.send("All votes summary.",
                                        file=discord.File(fp=tfile.name, filename=f"AllVotes.{now.day}_{now.month}_{now.hour}{now.minute}.txt"))


    def vote_direction(self, globe):
        """Display vote direction"""
        if globe.show_vote_goal or is_near_tick():
+11 −13
Original line number Diff line number Diff line
@@ -305,26 +305,24 @@ class CogBot(discord.Client):
                    exc.message = 'Invalid command use. Check the command help.'
                    exc.message += '\n{}\n{}'.format(len(exc.message) * '-', exc2.message)

            await self.send_ttl_message(channel, exc.reply())
            await self.send_ttl_message(channel, str(exc))
            try:
                if edit_time == message.edited_at:
                    await message.delete()
            except discord.DiscordException:
                pass

        except cog.exc.UserException as exc:
        except cog.exc.CogException as exc:
            exc.write_log(log, content=content, author=author, channel=channel)

            await self.send_ttl_message(channel, exc.reply())
            if isinstance(exc, cog.exc.UserException):
                await self.send_ttl_message(channel, str(exc))
                try:
                    if edit_time == message.edited_at:
                        await message.delete()
                except discord.DiscordException:
                    pass

        except cog.exc.InternalException as exc:
            exc.write_log(log, content=content, author=author, channel=channel)
            await self.send_message(channel, exc.reply())
            else:
                await self.send_message(channel, str(exc))

        except discord.DiscordException as exc:
            if exc.args[0].startswith("BAD REQUEST (status code: 400"):
+36 −43
Original line number Diff line number Diff line
"""
Common exceptions.
"""
import cog.util
import sqlalchemy.orm.exc as sqla_oexc

from cog.matching import substr_ind, DUMMY_ATTRIBUTE


class CogException(Exception):
@@ -11,27 +13,17 @@ class CogException(Exception):
        - Reply to the user with some relevant response.
    """
    def __init__(self, msg=None, lvl='info'):
        super().__init__()
        super().__init__(msg)
        self.log_level = lvl
        self.message = msg

    def write_log(self, log, *, content, author, channel):
        """
        Log all relevant message about this session.
        """
        log_func = getattr(log, self.log_level)
        header = '\n{}\n{}\n'.format(self.__class__.__name__ + ': ' + self.reply(), '=' * 20)
        header = '\n{}\n{}\n'.format(self.__class__.__name__ + ': ' + str(self), '=' * 20)
        log_func(header + log_format(content=content, author=author, channel=channel))

    def reply(self):
        """
        Construct a reponse to user.
        """
        return self.message

    def __str__(self):
        return str(self.reply())


class UserException(CogException):
    """
@@ -59,39 +51,37 @@ class InvalidPerms(UserException):

class MoreThanOneMatch(UserException):
    """ Too many matches were found for sequence.  """
    def __init__(self, sequence, matches, obj_attr=None):
        super().__init__()
        self.sequence = sequence
        self.matches = matches
        self.obj_attr = obj_attr if obj_attr else ''

    def reply(self):
        obj = self.matches[0]
        if not obj or isinstance(obj, type('')):
            cls = 'string'
        else:
            cls = self.matches[0].__class__.__name__

        header = "Resubmit query with more specific criteria."
        header += "\nToo many matches for '{}' in {}s:".format(
            self.sequence, cls)
        matched_strings = [emphasize_match(self.sequence, getattr(obj, self.obj_attr, obj))
                           for obj in self.matches]
        matched = "\n    - " + "\n    - ".join(matched_strings)
        return header + matched
    def __init__(self, needle, haystack, type_name, obj_attr=DUMMY_ATTRIBUTE):
        super().__init__('Empty')
        self.needle = needle
        self.haystack = haystack
        self.type_name = type_name
        self.obj_attr = obj_attr

    def __str__(self):
        matches = [emphasize_match(self.needle, getattr(obj, self.obj_attr, obj))
                   for obj in self.haystack]
        matches = "    - " + "\n    - ".join(matches)
        return f"""Unable to match exactly one result. Refine the search.

Looked for __{self.needle}__ in {self.type_name}s. Potentially matched the following:

{matches}"""


class NoMatch(UserException):
    """
    No match was found for sequence.
    """
    def __init__(self, sequence, obj_type):
        super().__init__()
        self.sequence = sequence
        self.obj_type = obj_type
    def __init__(self, needle, type_name):
        super().__init__('Empty')
        self.needle = needle
        self.type_name = type_name

    def reply(self):
        return "No matches for '{}' in {}s.".format(self.sequence, self.obj_type)
    def __str__(self):
        return f"""No match when one was required. Refine the search.

Looked for for __{self.needle}__ in {self.type_name}s."""


class CmdAborted(UserException):
@@ -164,7 +154,7 @@ class NameCollisionError(SheetParsingError):
        self.sheet = sheet
        self.rows = rows

    def reply(self):
    def __str__(self):
        lines = [
            "**Critical Error**",
            "----------------",
@@ -182,9 +172,12 @@ def emphasize_match(seq, line, fmt='__{}__'):
    """
    Emphasize the matched portion of string.
    """
    start, end = cog.util.substr_ind(seq, line)
    matched = line[start:end]
    return line.replace(matched, fmt.format(matched))
    indices = substr_ind(seq.lower(), line.lower(), skip_spaces=True)
    if indices:
        matched = line[indices[0]:indices[1]]
        line = line.replace(matched, fmt.format(matched))

    return line


def log_format(*, content, author, channel):

cog/matching.py

0 → 100644
+41 −0
Original line number Diff line number Diff line
"""
Exists to unwind a circular import between cog.exc and cog.util.
"""
DUMMY_ATTRIBUTE = "zzzzz"  # I don't expect this attribute anywhere.


def substr_ind(seq, line, *, skip_spaces=False):
    """Find the substring indexes (start, end) of a given sequence in a line.

    If you wish to ignore case, lower case before calling.

    Args:
        seq: A sequence of characters to look for in line, may contain spaces.
        line: A line of text to scan accross.
        skip_spaces: If true, ignore spaces when matching.
    """
    if skip_spaces:
        seq = seq.replace(' ', '')

    if len(line) < len(seq):
        return []

    start = None
    count = 0
    num_required = len(seq)
    for ind, char in enumerate(line):
        if skip_spaces and char == ' ':
            continue

        if char == seq[count]:
            if count == 0:
                start = ind
            count += 1
        else:
            count = 0
            start = None

        if count == num_required:
            return [start, ind + 1]

    return []
+29 −34
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ except ImportError:

import cog.config
import cog.exc
from cog.matching import substr_ind, DUMMY_ATTRIBUTE

BOT = None
MSG_LIMIT = 1950  # Number chars before message truncation
@@ -150,47 +151,41 @@ class WaitCB():
            await self.resume_cb()


def substr_match(seq, line, *, skip_spaces=True, ignore_case=True):
    """
    True iff the substr is present in string. Ignore spaces and optionally case.
    """
    return substr_ind(seq, line, skip_spaces=skip_spaces,
                      ignore_case=ignore_case) != []
# TODO: Name? This isn't the fuzzy find but I crap at naming.
def fuzzy_find(needle, stack, *, obj_attr=DUMMY_ATTRIBUTE, obj_type='String', ignore_case=True, skip_spaces=True):
    """Search for needle in stack with optional flags.

    This is essentially a `needle in stack` test except for the extra flags.
    It is expected to return exactly __ONE__ object from the stack.

def substr_ind(seq, line, *, skip_spaces=True, ignore_case=True):
    """
    Return the start and end + 1 index of a substring match of seq to line.

    Returns:
        [start, end + 1] if needle found in line
        [] if needle not found in line
    Args:
        needle: What you are looking for.
        stack: The collection of things you are looking in.
        obj_attr: Optional attribute to look at on every object in the stack to match.
                  If set, matches will still return the original object.
        ignore_case: If true, ignore case in both the needle and stack.
        skip_spaces: If true, ignore spaces in both the needle and stack.

    Raises:
        cog.exc.NoMatch: No match was found, expected to find one.
        cog.exc.MoreThanOneMatch: Too many matches were found, expected to find one.
                                  If you want all matches, exception has them in exc.matches.
    """
    if ignore_case:
        seq = seq.lower()
        line = line.lower()

    if skip_spaces:
        seq = seq.replace(' ', '')

    start = None
    count = 0
    for ind, char in enumerate(line):
        if skip_spaces and char == ' ':
            continue
        needle = needle.lower()

        if char == seq[count]:
            if count == 0:
                start = ind
            count += 1
        else:
            count = 0
            start = None
    matches = []
    for obj in stack:
        line = getattr(obj, obj_attr, obj)
        if substr_ind(needle, line.lower() if ignore_case else line, skip_spaces=skip_spaces):
            matches.append(obj)

        if count == len(seq):
            return [start, ind + 1]
    if len(matches) == 0:
        raise cog.exc.NoMatch(needle, obj_type)
    if len(matches) != 1:
        raise cog.exc.MoreThanOneMatch(needle, matches, obj_type, obj_attr)

    return []
    return matches[0]


def rel_to_abs(*path_parts):
Loading