Ejemplo n.º 1
0
    def __init__(self, client, config=None):
        self.client = client
        self.config = config or BotConfig()

        # Shard manager
        self.shards = None

        # The context carries information about events in a threadlocal storage
        self.ctx = ThreadLocal()

        # The storage object acts as a dynamic contextual aware store
        self.storage = None
        if self.config.storage_enabled:
            self.storage = Storage(self.ctx,
                                   self.config.from_prefix('storage'))

        # If the manhole is enabled, add this bot as a local
        if self.client.config.manhole_enable:
            self.client.manhole_locals['bot'] = self

        if self.config.http_enabled:
            from flask import Flask
            self.log.info('Starting HTTP server bound to %s:%s',
                          self.config.http_host, self.config.http_port)
            self.http = Flask('disco')
            self.http_server = WSGIServer(
                (self.config.http_host, self.config.http_port), self.http)
            self.http_server_greenlet = gevent.spawn(
                self.http_server.serve_forever)

        self.plugins = {}
        self.group_abbrev = {}

        # Only bind event listeners if we're going to parse commands
        if self.config.commands_enabled:
            self.client.events.on('MessageCreate', self.on_message_create)

            if self.config.commands_allow_edit:
                self.client.events.on('MessageUpdate', self.on_message_update)

        # If we have a level getter and its a string, try to load it
        if isinstance(self.config.commands_level_getter, six.string_types):
            mod, func = self.config.commands_level_getter.rsplit('.', 1)
            mod = importlib.import_module(mod)
            self.config.commands_level_getter = getattr(mod, func)

        # Stores the last message for every single channel
        self.last_message_cache = {}

        # Stores a giant regex matcher for all commands
        self.command_matches_re = None

        # Finally, load all the plugin modules that where passed with the config
        for plugin_mod in self.config.plugins:
            self.add_plugin_module(plugin_mod)

        # Convert level mapping
        for k, v in six.iteritems(self.config.levels):
            self.config.levels[k] = CommandLevels.get(v)
Ejemplo n.º 2
0
    def __init__(self, client, config=None):
        self.client = client
        self.config = config or BotConfig()

        # Shard manager
        self.shards = None

        # The context carries information about events in a threadlocal storage
        self.ctx = ThreadLocal()

        # The storage object acts as a dynamic contextual aware store
        self.storage = None
        if self.config.storage_enabled:
            self.storage = Storage(self.ctx, self.config.from_prefix('storage'))

        # If the manhole is enabled, add this bot as a local
        if self.client.config.manhole_enable:
            self.client.manhole_locals['bot'] = self

        self.plugins = {}
        self.group_abbrev = {}

        # Only bind event listeners if we're going to parse commands
        if self.config.commands_enabled:
            self.client.events.on('MessageCreate', self.on_message_create)

            if self.config.commands_allow_edit:
                self.client.events.on('MessageUpdate', self.on_message_update)

        # Stores the last message for every single channel
        self.last_message_cache = {}

        # Stores a giant regex matcher for all commands
        self.command_matches_re = None

        # Finally, load all the plugin modules that where passed with the config
        for plugin_mod in self.config.plugins:
            self.add_plugin_module(plugin_mod)

        # Convert level mapping
        for k, v in six.iteritems(self.config.levels):
            self.config.levels[k] = CommandLevels.get(v)
Ejemplo n.º 3
0
class Bot(LoggingClass):
    """
    Disco's implementation of a simple but extendable Discord bot. Bots consist
    of a set of plugins, and a Disco client.

    Parameters
    ----------
    client : :class:`disco.client.Client`
        The client this bot should utilize for its connection.
    config : Optional[:class:`BotConfig`]
        The configuration to use for this bot. If not provided will use the defaults
        inside of :class:`BotConfig`.

    Attributes
    ----------
    client : `disco.client.Client`
        The client instance for this bot.
    config : `BotConfig`
        The bot configuration instance for this bot.
    plugins : dict(str, :class:`disco.bot.plugin.Plugin`)
        Any plugins this bot has loaded
    """
    def __init__(self, client, config=None):
        self.client = client
        self.config = config or BotConfig()

        # Shard manager
        self.shards = None

        # The context carries information about events in a threadlocal storage
        self.ctx = ThreadLocal()

        # The storage object acts as a dynamic contextual aware store
        self.storage = None
        if self.config.storage_enabled:
            self.storage = Storage(self.ctx,
                                   self.config.from_prefix('storage'))

        # If the manhole is enabled, add this bot as a local
        if self.client.config.manhole_enable:
            self.client.manhole_locals['bot'] = self

        if self.config.http_enabled:
            from flask import Flask
            self.log.info('Starting HTTP server bound to %s:%s',
                          self.config.http_host, self.config.http_port)
            self.http = Flask('disco')
            self.http_server = WSGIServer(
                (self.config.http_host, self.config.http_port), self.http)
            self.http_server_greenlet = gevent.spawn(
                self.http_server.serve_forever)

        self.plugins = {}
        self.group_abbrev = {}

        # Only bind event listeners if we're going to parse commands
        if self.config.commands_enabled:
            self.client.events.on('MessageCreate', self.on_message_create)

            if self.config.commands_allow_edit:
                self.client.events.on('MessageUpdate', self.on_message_update)

        # If we have a level getter and its a string, try to load it
        if isinstance(self.config.commands_level_getter, six.string_types):
            mod, func = self.config.commands_level_getter.rsplit('.', 1)
            mod = importlib.import_module(mod)
            self.config.commands_level_getter = getattr(mod, func)

        # Stores the last message for every single channel
        self.last_message_cache = {}

        # Stores a giant regex matcher for all commands
        self.command_matches_re = None

        # Finally, load all the plugin modules that where passed with the config
        for plugin_mod in self.config.plugins:
            self.add_plugin_module(plugin_mod)

        # Convert level mapping
        for k, v in six.iteritems(self.config.levels):
            self.config.levels[k] = CommandLevels.get(v)

    @classmethod
    def from_cli(cls, *plugins):
        """
        Creates a new instance of the bot using the utilities inside of the
        :mod:`disco.cli` module. Allows passing in a set of uninitialized
        plugin classes to load.

        Parameters
        ---------
        plugins : Optional[list(:class:`disco.bot.plugin.Plugin`)]
            Any plugins to load after creating the new bot instance

        """
        from disco.cli import disco_main
        inst = cls(disco_main())

        for plugin in plugins:
            inst.add_plugin(plugin)

        return inst

    @property
    def commands(self):
        """
        Generator of all commands this bots plugins have defined.
        """
        for plugin in six.itervalues(self.plugins):
            for command in plugin.commands:
                yield command

    def recompute(self):
        """
        Called when a plugin is loaded/unloaded to recompute internal state.
        """
        if self.config.commands_group_abbrev:
            groups = {
                command.group
                for command in self.commands if command.group
            }
            self.group_abbrev = self.compute_group_abbrev(groups)

        self.compute_command_matches_re()

    def compute_group_abbrev(self, groups):
        """
        Computes all possible abbreviations for a command grouping.
        """
        # For the first pass, we just want to compute each groups possible
        #  abbreviations that don't conflict with eachother.
        possible = {}
        for group in groups:
            for index in range(1, len(group)):
                current = group[:index]
                if current in possible:
                    possible[current] = None
                else:
                    possible[current] = group

        # Now, we want to compute the actual shortest abbreivation out of the
        #  possible ones
        result = {}
        for abbrev, group in six.iteritems(possible):
            if not group:
                continue

            if group in result:
                if len(abbrev) < len(result[group]):
                    result[group] = abbrev
            else:
                result[group] = abbrev

        return result

    def compute_command_matches_re(self):
        """
        Computes a single regex which matches all possible command combinations.
        """
        commands = list(self.commands)
        re_str = '|'.join(command.regex(grouped=False) for command in commands)
        if re_str:
            self.command_matches_re = re.compile(re_str, re.I)
        else:
            self.command_matches_re = None

    def get_commands_for_message(self, require_mention, mention_rules, prefix,
                                 msg):
        """
        Generator of all commands that a given message object triggers, based on
        the bots plugins and configuration.

        Parameters
        ---------
        msg : :class:`disco.types.message.Message`
            The message object to parse and find matching commands for

        Yields
        -------
        tuple(:class:`disco.bot.command.Command`, `re.MatchObject`)
            All commands the message triggers
        """
        content = msg.content

        if require_mention:
            mention_direct = msg.is_mentioned(self.client.state.me)
            mention_everyone = msg.mention_everyone

            mention_roles = []
            if msg.guild:
                mention_roles = list(
                    filter(lambda r: msg.is_mentioned(r),
                           msg.guild.get_member(self.client.state.me).roles))

            if not any((
                    mention_rules.get('user', True) and mention_direct,
                    mention_rules.get('everyone', False) and mention_everyone,
                    mention_rules.get('role', False) and any(mention_roles),
                    msg.channel.is_dm,
            )):
                return []

            if mention_direct:
                if msg.guild:
                    member = msg.guild.get_member(self.client.state.me)
                    if member:
                        # If nickname is set, filter both the normal and nick mentions
                        if member.nick:
                            content = content.replace(member.mention, '', 1)
                        content = content.replace(member.user.mention, '', 1)
                else:
                    content = content.replace(self.client.state.me.mention, '',
                                              1)
            elif mention_everyone:
                content = content.replace('@everyone', '', 1)
            else:
                for role in mention_roles:
                    content = content.replace('<@{}>'.format(role), '', 1)

            content = content.lstrip()

        if prefix and not content.startswith(prefix):
            return []
        else:
            content = content[len(prefix):]

        if not self.command_matches_re or not self.command_matches_re.match(
                content):
            return []

        options = []
        for command in self.commands:
            match = command.compiled_regex.match(content)
            if match:
                options.append((command, match))
        return sorted(options, key=lambda obj: obj[0].group is None)

    def get_level(self, actor):
        level = CommandLevels.DEFAULT

        if callable(self.config.commands_level_getter):
            level = self.config.commands_level_getter(self, actor)
        else:
            if actor.id in self.config.levels:
                level = self.config.levels[actor.id]

            if isinstance(actor, GuildMember):
                for rid in actor.roles:
                    if rid in self.config.levels and self.config.levels[
                            rid] > level:
                        level = self.config.levels[rid]

        return level

    def check_command_permissions(self, command, msg):
        if not command.level:
            return True

        level = self.get_level(
            msg.author if not msg.guild else msg.guild.get_member(msg.author))

        if level >= command.level:
            return True
        return False

    def handle_message(self, msg):
        """
        Attempts to handle a newly created or edited message in the context of
        command parsing/triggering. Calls all relevant commands the message triggers.

        Parameters
        ---------
        msg : :class:`disco.types.message.Message`
            The newly created or updated message object to parse/handle.

        Returns
        -------
        bool
            whether any commands where successfully triggered by the message
        """
        commands = list(
            self.get_commands_for_message(
                self.config.commands_require_mention,
                self.config.commands_mention_rules,
                self.config.commands_prefix,
                msg,
            ))

        if not len(commands):
            return False

        for command, match in commands:
            if not self.check_command_permissions(command, msg):
                continue

            if command.plugin.execute(CommandEvent(command, msg, match)):
                return True
        return False

    def on_message_create(self, event):
        if event.message.author.id == self.client.state.me.id:
            return

        result = self.handle_message(event.message)

        if self.config.commands_allow_edit:
            self.last_message_cache[event.message.channel_id] = (event.message,
                                                                 result)

    def on_message_update(self, event):
        if self.config.commands_allow_edit:
            obj = self.last_message_cache.get(event.message.channel_id)
            if not obj:
                return

            msg, triggered = obj
            if msg.id == event.message.id and not triggered:
                msg.inplace_update(event.message)
                triggered = self.handle_message(msg)

                self.last_message_cache[msg.channel_id] = (msg, triggered)

    def add_plugin(self, inst, config=None, ctx=None):
        """
        Adds and loads a plugin, based on its class.

        Parameters
        ----------
        inst : subclass (or instance therein) of `disco.bot.plugin.Plugin`
            Plugin class to initialize and load.
        config : Optional
            The configuration to load the plugin with.
        ctx : Optional[dict]
            Context (previous state) to pass the plugin. Usually used along w/
            unload.
        """
        if inspect.isclass(inst):
            if not config:
                if callable(self.config.plugin_config_provider):
                    config = self.config.plugin_config_provider(inst)
                else:
                    config = self.load_plugin_config(inst)

            inst = inst(self, config)

        if inst.__class__.__name__ in self.plugins:
            self.log.warning('Attempted to add already added plugin %s',
                             inst.__class__.__name__)
            raise Exception('Cannot add already added plugin: {}'.format(
                inst.__class__.__name__))

        self.ctx['plugin'] = self.plugins[inst.__class__.__name__] = inst
        self.plugins[inst.__class__.__name__].load(ctx or {})
        self.recompute()
        self.ctx.drop()

    def rmv_plugin(self, cls):
        """
        Unloads and removes a plugin based on its class.

        Parameters
        ----------
        cls : subclass of :class:`disco.bot.plugin.Plugin`
            Plugin class to unload and remove.
        """
        if cls.__name__ not in self.plugins:
            raise Exception('Cannot remove non-existant plugin: {}'.format(
                cls.__name__))

        ctx = {}
        self.plugins[cls.__name__].unload(ctx)
        del self.plugins[cls.__name__]
        self.recompute()
        return ctx

    def reload_plugin(self, cls):
        """
        Reloads a plugin.
        """
        config = self.plugins[cls.__name__].config

        ctx = self.rmv_plugin(cls)
        module = reload_module(inspect.getmodule(cls))
        self.add_plugin(getattr(module, cls.__name__), config, ctx)

    def run_forever(self):
        """
        Runs this bots core loop forever.
        """
        self.client.run_forever()

    def add_plugin_module(self, path, config=None):
        """
        Adds and loads a plugin, based on its module path.
        """
        self.log.info('Adding plugin module at path "%s"', path)
        mod = importlib.import_module(path)
        loaded = False

        for entry in map(lambda i: getattr(mod, i), dir(mod)):
            if inspect.isclass(entry) and issubclass(
                    entry, Plugin) and not entry == Plugin:
                if getattr(entry, '_shallow',
                           False) and Plugin in entry.__bases__:
                    continue
                loaded = True
                self.add_plugin(entry, config)

        if not loaded:
            raise Exception(
                'Could not find any plugins to load within module {}'.format(
                    path))

    def load_plugin_config(self, cls):
        name = cls.__name__.lower()
        if name.endswith('plugin'):
            name = name[:-6]

        path = os.path.join(self.config.plugin_config_dir,
                            name) + '.' + self.config.plugin_config_format

        data = {}
        if self.config.shared_config:
            data.update(self.config.shared_config)

        if name in self.config.plugin_config:
            data.update(self.config.plugin_config[name])

        if os.path.exists(path):
            with open(path, 'r') as f:
                data.update(
                    Serializer.loads(self.config.plugin_config_format,
                                     f.read()))

        if hasattr(cls, 'config_cls'):
            inst = cls.config_cls()
            if data:
                inst.update(data)
            return inst

        return data
Ejemplo n.º 4
0
class Bot(object):
    """
    Disco's implementation of a simple but extendable Discord bot. Bots consist
    of a set of plugins, and a Disco client.

    Parameters
    ----------
    client : :class:`disco.client.Client`
        The client this bot should utilize for its connection.
    config : Optional[:class:`BotConfig`]
        The configuration to use for this bot. If not provided will use the defaults
        inside of :class:`BotConfig`.

    Attributes
    ----------
    client : `disco.client.Client`
        The client instance for this bot.
    config : `BotConfig`
        The bot configuration instance for this bot.
    plugins : dict(str, :class:`disco.bot.plugin.Plugin`)
        Any plugins this bot has loaded
    """
    def __init__(self, client, config=None):
        self.client = client
        self.config = config or BotConfig()

        # Shard manager
        self.shards = None

        # The context carries information about events in a threadlocal storage
        self.ctx = ThreadLocal()

        # The storage object acts as a dynamic contextual aware store
        self.storage = None
        if self.config.storage_enabled:
            self.storage = Storage(self.ctx, self.config.from_prefix('storage'))

        # If the manhole is enabled, add this bot as a local
        if self.client.config.manhole_enable:
            self.client.manhole_locals['bot'] = self

        self.plugins = {}
        self.group_abbrev = {}

        # Only bind event listeners if we're going to parse commands
        if self.config.commands_enabled:
            self.client.events.on('MessageCreate', self.on_message_create)

            if self.config.commands_allow_edit:
                self.client.events.on('MessageUpdate', self.on_message_update)

        # Stores the last message for every single channel
        self.last_message_cache = {}

        # Stores a giant regex matcher for all commands
        self.command_matches_re = None

        # Finally, load all the plugin modules that where passed with the config
        for plugin_mod in self.config.plugins:
            self.add_plugin_module(plugin_mod)

        # Convert level mapping
        for k, v in six.iteritems(self.config.levels):
            self.config.levels[k] = CommandLevels.get(v)

    @classmethod
    def from_cli(cls, *plugins):
        """
        Creates a new instance of the bot using the utilities inside of the
        :mod:`disco.cli` module. Allows passing in a set of uninitialized
        plugin classes to load.

        Parameters
        ---------
        plugins : Optional[list(:class:`disco.bot.plugin.Plugin`)]
            Any plugins to load after creating the new bot instance

        """
        from disco.cli import disco_main
        inst = cls(disco_main())

        for plugin in plugins:
            inst.add_plugin(plugin)

        return inst

    @property
    def commands(self):
        """
        Generator of all commands this bots plugins have defined.
        """
        for plugin in six.itervalues(self.plugins):
            for command in plugin.commands:
                yield command

    def recompute(self):
        """
        Called when a plugin is loaded/unloaded to recompute internal state.
        """
        if self.config.commands_group_abbrev:
            self.compute_group_abbrev()

        self.compute_command_matches_re()

    def compute_group_abbrev(self):
        """
        Computes all possible abbreviations for a command grouping.
        """
        self.group_abbrev = {}
        groups = set(command.group for command in self.commands if command.group)

        for group in groups:
            grp = group
            while grp:
                # If the group already exists, means someone else thought they
                #  could use it so we need yank it from them (and not use it)
                if grp in list(six.itervalues(self.group_abbrev)):
                    self.group_abbrev = {k: v for k, v in six.iteritems(self.group_abbrev) if v != grp}
                else:
                    self.group_abbrev[group] = grp

                grp = grp[:-1]

    def compute_command_matches_re(self):
        """
        Computes a single regex which matches all possible command combinations.
        """
        commands = list(self.commands)
        re_str = '|'.join(command.regex for command in commands)
        if re_str:
            self.command_matches_re = re.compile(re_str)
        else:
            self.command_matches_re = None

    def get_commands_for_message(self, require_mention, mention_rules, prefix, msg):
        """
        Generator of all commands that a given message object triggers, based on
        the bots plugins and configuration.

        Parameters
        ---------
        msg : :class:`disco.types.message.Message`
            The message object to parse and find matching commands for

        Yields
        -------
        tuple(:class:`disco.bot.command.Command`, `re.MatchObject`)
            All commands the message triggers
        """
        content = msg.content

        if require_mention:
            mention_direct = msg.is_mentioned(self.client.state.me)
            mention_everyone = msg.mention_everyone

            mention_roles = []
            if msg.guild:
                mention_roles = list(filter(lambda r: msg.is_mentioned(r),
                                            msg.guild.get_member(self.client.state.me).roles))

            if not any((
                mention_rules.get('user', True) and mention_direct,
                mention_rules.get('everyone', False) and mention_everyone,
                mention_rules.get('role', False) and any(mention_roles),
                msg.channel.is_dm
            )):
                raise StopIteration

            if mention_direct:
                if msg.guild:
                    member = msg.guild.get_member(self.client.state.me)
                    if member:
                        content = content.replace(member.mention, '', 1)
                else:
                    content = content.replace(self.client.state.me.mention, '', 1)
            elif mention_everyone:
                content = content.replace('@everyone', '', 1)
            else:
                for role in mention_roles:
                    content = content.replace('<@{}>'.format(role), '', 1)

            content = content.lstrip()

        if prefix and not content.startswith(prefix):
            raise StopIteration
        else:
            content = content[len(prefix):]

        if not self.command_matches_re or not self.command_matches_re.match(content):
            raise StopIteration

        for command in self.commands:
            match = command.compiled_regex.match(content)
            if match:
                yield (command, match)

    def get_level(self, actor):
        level = CommandLevels.DEFAULT

        if callable(self.config.commands_level_getter):
            level = self.config.commands_level_getter(actor)
        else:
            if actor.id in self.config.levels:
                level = self.config.levels[actor.id]

            if isinstance(actor, GuildMember):
                for rid in actor.roles:
                    if rid in self.config.levels and self.config.levels[rid] > level:
                        level = self.config.levels[rid]

        return level

    def check_command_permissions(self, command, msg):
        if not command.level:
            return True

        level = self.get_level(msg.author if not msg.guild else msg.guild.get_member(msg.author))

        if level >= command.level:
            return True
        return False

    def handle_message(self, msg):
        """
        Attempts to handle a newly created or edited message in the context of
        command parsing/triggering. Calls all relevant commands the message triggers.

        Parameters
        ---------
        msg : :class:`disco.types.message.Message`
            The newly created or updated message object to parse/handle.

        Returns
        -------
        bool
            whether any commands where successfully triggered by the message
        """
        commands = list(self.get_commands_for_message(
            self.config.commands_require_mention,
            self.config.commands_mention_rules,
            self.config.commands_prefix,
            msg
        ))

        if not len(commands):
            return False

        result = False
        for command, match in commands:
            if not self.check_command_permissions(command, msg):
                continue

            if command.plugin.execute(CommandEvent(command, msg, match)):
                result = True
        return result

    def on_message_create(self, event):
        if event.message.author.id == self.client.state.me.id:
            return

        if self.config.commands_allow_edit:
            self.last_message_cache[event.message.channel_id] = (event.message, False)

        self.handle_message(event.message)

    def on_message_update(self, event):
        if self.config.commands_allow_edit:
            obj = self.last_message_cache.get(event.message.channel_id)
            if not obj:
                return

            msg, triggered = obj
            if msg.id == event.message.id and not triggered:
                msg.update(event.message)
                triggered = self.handle_message(msg)

                self.last_message_cache[msg.channel_id] = (msg, triggered)

    def add_plugin(self, cls, config=None, ctx=None):
        """
        Adds and loads a plugin, based on its class.

        Parameters
        ----------
        cls : subclass of :class:`disco.bot.plugin.Plugin`
            Plugin class to initialize and load.
        config : Optional
            The configuration to load the plugin with.
        ctx : Optional[dict]
            Context (previous state) to pass the plugin. Usually used along w/
            unload.
        """
        if cls.__name__ in self.plugins:
            raise Exception('Cannot add already added plugin: {}'.format(cls.__name__))

        if not config:
            if callable(self.config.plugin_config_provider):
                config = self.config.plugin_config_provider(cls)
            else:
                config = self.load_plugin_config(cls)

        self.ctx['plugin'] = self.plugins[cls.__name__] = cls(self, config)
        self.plugins[cls.__name__].load(ctx or {})
        self.recompute()
        self.ctx.drop()

    def rmv_plugin(self, cls):
        """
        Unloads and removes a plugin based on its class.

        Parameters
        ----------
        cls : subclass of :class:`disco.bot.plugin.Plugin`
            Plugin class to unload and remove.
        """
        if cls.__name__ not in self.plugins:
            raise Exception('Cannot remove non-existant plugin: {}'.format(cls.__name__))

        ctx = {}
        self.plugins[cls.__name__].unload(ctx)
        del self.plugins[cls.__name__]
        self.recompute()
        return ctx

    def reload_plugin(self, cls):
        """
        Reloads a plugin.
        """
        config = self.plugins[cls.__name__].config

        ctx = self.rmv_plugin(cls)
        module = reload_module(inspect.getmodule(cls))
        self.add_plugin(getattr(module, cls.__name__), config, ctx)

    def run_forever(self):
        """
        Runs this bots core loop forever.
        """
        self.client.run_forever()

    def add_plugin_module(self, path, config=None):
        """
        Adds and loads a plugin, based on its module path.
        """

        mod = importlib.import_module(path)
        loaded = False

        for entry in map(lambda i: getattr(mod, i), dir(mod)):
            if inspect.isclass(entry) and issubclass(entry, Plugin) and not entry == Plugin:
                if getattr(entry, '_shallow', False) and Plugin in entry.__bases__:
                    continue
                loaded = True
                self.add_plugin(entry, config)

        if not loaded:
            raise Exception('Could not find any plugins to load within module {}'.format(path))

    def load_plugin_config(self, cls):
        name = cls.__name__.lower()
        if name.endswith('plugin'):
            name = name[:-6]

        path = os.path.join(
            self.config.plugin_config_dir, name) + '.' + self.config.plugin_config_format

        if not os.path.exists(path):
            if hasattr(cls, 'config_cls'):
                return cls.config_cls()
            return

        with open(path, 'r') as f:
            data = Serializer.loads(self.config.plugin_config_format, f.read())

        if hasattr(cls, 'config_cls'):
            inst = cls.config_cls()
            inst.update(data)
            return inst

        return data