Skip to content

Commit

Permalink
Merge pull request #1973 from Exirel/config-unexpected-settings
Browse files Browse the repository at this point in the history
config: warn if unexpected settings exist
  • Loading branch information
dgw authored Dec 25, 2020
2 parents 3a81b0c + 35e4916 commit 2439128
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 7 deletions.
38 changes: 37 additions & 1 deletion sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -326,6 +326,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.
Expand Down Expand Up @@ -452,6 +480,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',
Expand Down Expand Up @@ -609,6 +639,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():
Expand Down Expand Up @@ -856,6 +888,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.
Expand Down Expand Up @@ -969,6 +1003,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``.
Expand Down
24 changes: 24 additions & 0 deletions sopel/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions sopel/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions test/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"#startquote
&endquote"
"&quoted"
[somesection]
is_defined = no
""" # noqa (trailing whitespaces are intended)

TEST_CHANNELS = [
Expand Down Expand Up @@ -343,3 +346,21 @@ def test_save_modified_config(multi_fakeconfig):
'&endquote"',
'"&quoted"', # 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')

0 comments on commit 2439128

Please sign in to comment.