From 096135e25f932127213a333327a81a3e0a30d816 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 30 Oct 2020 15:27:14 +0100 Subject: [PATCH 1/2] config: add Config.get_defined_sections --- sopel/config/__init__.py | 24 ++++++++++++++++++++++++ sopel/config/types.py | 15 +++++++++------ test/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/sopel/config/__init__.py b/sopel/config/__init__.py index 4290b8a4cc..62174dc1dc 100644 --- a/sopel/config/__init__.py +++ b/sopel/config/__init__.py @@ -159,6 +159,30 @@ def homedir(self): else: return os.path.dirname(self.filename) + def get_defined_sections(self): + """Retrieve all defined static sections of this configuration. + + :return: all instances of :class:`~sopel.config.types.StaticSection` + defined for this configuration file + :rtype: list + + When a plugin defines a section (using :meth:`define_section`), it + associates a :class:`~sopel.config.types.StaticSection` for the section. + This method allows to retrieve these instances of ``StaticSection``, + and only these. + + .. versionadded:: 7.1 + """ + sections = ( + (name, getattr(self, name)) + for name in self.parser.sections() + ) + return [ + (name, section) + for name, section in sections + if isinstance(section, types.StaticSection) + ] + def save(self): """Write all changes to the config file. diff --git a/sopel/config/types.py b/sopel/config/types.py index cc09a5dfb6..465ae601f9 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -66,20 +66,23 @@ def __init__(self, config, section_name, validate=True): self._parent = config self._parser = config.parser self._section_name = section_name + for value in dir(self): + if value in ('_parent', '_parser', '_section_name'): + # ignore internal attributes + continue + try: getattr(self, value) except ValueError as e: raise ValueError( - 'Invalid value for {}.{}: {}'.format(section_name, value, - str(e)) - ) + 'Invalid value for {}.{}: {}'.format( + section_name, value, str(e))) except AttributeError: if validate: raise ValueError( - 'Missing required value for {}.{}'.format(section_name, - value) - ) + 'Missing required value for {}.{}'.format( + section_name, value)) def configure_setting(self, name, prompt, default=NO_DEFAULT): """Return a validated value for this attribute from the terminal. diff --git a/test/test_config.py b/test/test_config.py index 1fb579e7a8..064f14c000 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -36,6 +36,9 @@ "#startquote &endquote" ""ed" + +[somesection] +is_defined = no """ # noqa (trailing whitespaces are intended) TEST_CHANNELS = [ @@ -343,3 +346,21 @@ def test_save_modified_config(multi_fakeconfig): '&endquote"', '""ed"', # doesn't start with a # so it isn't escaped ] + + +def test_get_defined_sections(multi_fakeconfig): + assert multi_fakeconfig.parser.has_section('core') + assert multi_fakeconfig.parser.has_section('fake') + assert multi_fakeconfig.parser.has_section('spam') + assert multi_fakeconfig.parser.has_section('somesection') + + results = multi_fakeconfig.get_defined_sections() + + assert len(results) == 3, 'There should be 3 defined sections' + + items = dict(results) + assert 'core' in items, 'core must be always defined' + assert 'fake' in items + assert 'spam' in items + assert 'somesection' not in items, ( + 'somesection was not defined and should not appear as such') From 35e4916c55ed2e99507fe0ce67faa3a9c9567d46 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 30 Oct 2020 15:29:38 +0100 Subject: [PATCH 2/2] bot: check defined sections in post_setup --- sopel/bot.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/sopel/bot.py b/sopel/bot.py index 974ead850d..c84ffe9137 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -254,7 +254,7 @@ def setup(self): """ self.setup_logging() self.setup_plugins() - self._scheduler.start() + self.post_setup() def setup_logging(self): """Set up logging based on config options.""" @@ -322,6 +322,34 @@ def setup_plugins(self): else: LOGGER.warning("Warning: Couldn't load any plugins") + # post setup + + def post_setup(self): + """Perform post-setup actions. + + This method handles everything that should happen after all the plugins + are loaded, and before the bot can connect to the IRC server. + + At the moment, this method checks for undefined configuration options, + and starts the job scheduler. + + .. versionadded:: 7.1 + """ + settings = self.settings + for section_name, section in settings.get_defined_sections(): + for option_name in settings.parser.options(section_name): + if not hasattr(section, option_name): + LOGGER.warning( + 'Config option `%s.%s` is not defined by its section ' + 'and may not be recognized by Sopel.', + section_name, + option_name, + ) + + self._scheduler.start() + + # plugins management + def reload_plugin(self, name): """Reload a plugin. @@ -448,6 +476,8 @@ def get_plugin_meta(self, name): return self._plugins[name].get_meta_description() + # callable management + @deprecated( reason="Replaced by specific `unregister_*` methods.", version='7.1', @@ -605,6 +635,8 @@ def msg(self, recipient, text, max_messages=1): """ self.say(text, recipient, max_messages) + # message dispatch + def call_rule(self, rule, sopel, trigger): # rate limiting if not trigger.admin and not rule.is_unblockable(): @@ -852,6 +884,8 @@ def _update_running_triggers(self, running_triggers): self._running_triggers = [ t for t in running_triggers if t.is_alive()] + # event handlers + def on_scheduler_error(self, scheduler, exc): """Called when the Job Scheduler fails. @@ -965,6 +999,8 @@ def _shutdown(self): # Avoid calling shutdown methods if we already have. self.shutdown_methods = [] + # URL callbacks management + def register_url_callback(self, pattern, callback): """Register a ``callback`` for URLs matching the regex ``pattern``.