From 4f173c05e13e68f894cb685e5d89c7ba45d8fc8c Mon Sep 17 00:00:00 2001 From: dgw Date: Sat, 7 Sep 2019 20:55:02 -0500 Subject: [PATCH] config.types: doc ALL the things! There's a lot to parse (ha!) here, but most of it is pretty obvious in purpose. The one thing I think I should explain is why all the `parse()` and `serialize()` instances have their own docstrings now. It's because I was having fun! Just kidding (sort of). It's actually because Sphinx autodoc pulls the docstring from the parent class's method if a subclass's overriding method doesn't have a docstring of its own. In hindsight, writing new docstrings might have been *more* work than the alternative (setting `autodoc_inherit_docstrings = False` in `docs/conf.py` and checking to see if anything broke), but what's done is done at this point. We can still change the setting later. Co-Authored-By: Exirel --- sopel/config/types.py | 156 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 128 insertions(+), 28 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index 9445927e01..61b5e9d8d4 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -42,7 +42,8 @@ class NO_DEFAULT(object): class StaticSection(object): """A configuration section with parsed and validated settings. - This class is intended to be subclassed with added ``ValidatedAttribute``\\s. + This class is intended to be subclassed and customized with added + attributes containing :class:`BaseValidated`-based objects. """ def __init__(self, config, section_name, validate=True): if not config.parser.has_section(section_name): @@ -68,7 +69,10 @@ def __init__(self, config, section_name, validate=True): def configure_setting(self, name, prompt, default=NO_DEFAULT): """Return a validated value for this attribute from the terminal. - ``prompt`` will be the docstring of the attribute if not given. + :param str name: the name of the attribute to configure + :param str prompt: the prompt text to display in the terminal + :param default: the value to be used if the user does not enter one + :type default: depends on subclass If ``default`` is passed, it will be used if no value is given by the user. If it is not passed, the current value of the setting, or the @@ -97,18 +101,26 @@ def configure_setting(self, name, prompt, default=NO_DEFAULT): class BaseValidated(object): - """The base type for a descriptor in a ``StaticSection``.""" + """The base type for a setting descriptor in a :class:`StaticSection`. + + :param str name: the attribute name to use in the config file + :param default: the value to be returned if the setting has no value; + if not specified, defaults to :obj:`None` + :type default: str, optional + + ``default`` also can be set to :const:`sopel.config.types.NO_DEFAULT`, if + the value *must* be configured by the user (i.e. there is no suitable + default value). Trying to read an empty ``NO_DEFAULT`` value will raise + :class:`AttributeError`. + """ def __init__(self, name, default=None): - """ - ``name`` is the name of the setting in the section. - ``default`` is the value to be returned if the setting is not set. If - not given, AttributeError will be raised instead. - """ self.name = name self.default = default def configure(self, prompt, default, parent, section_name): - """With the prompt and default, parse and return a value from terminal. + """ + With the ``prompt`` and ``default``, parse and return a value from + terminal. """ if default is not NO_DEFAULT and default is not None: prompt = '{} [{}]'.format(prompt, default) @@ -180,14 +192,19 @@ def _serialize_boolean(value): class ValidatedAttribute(BaseValidated): + """A descriptor for settings in a :class:`StaticSection`. + + :param str name: the attribute name to use in the config file + :param parse: a function to be used to read the string and create the + appropriate object; the string value will be returned + as-is if not set + :type parse: :term:`function`, optional + :param serialize: a function that, given an object, should return a string + that can be written to the config file safely; defaults + to :class:`str` + :type serialize: :term:`function`, optional + """ def __init__(self, name, parse=None, serialize=None, default=None): - """A descriptor for settings in a ``StaticSection`` - - ``parse`` is the function to be used to read the string and create the - appropriate object. If not given, return the string as-is. - ``serialize`` takes an object, and returns the value to be written to - the file. If not given, defaults to ``unicode``. - """ self.name = name if parse == bool: parse = _parse_boolean @@ -198,12 +215,26 @@ def __init__(self, name, parse=None, serialize=None, default=None): self.default = default def serialize(self, value): + """Return the ``value`` as a Unicode string. + + :param value: the option value + :rtype: str + """ return unicode(value) def parse(self, value): + """No-op: simply returns the given ``value``, unchanged. + + :param str value: the string read from the config file + :rtype: str + """ return value def configure(self, prompt, default, parent, section_name): + """ + With the ``prompt`` and ``default``, parse and return a value from + terminal. + """ if self.parse == _parse_boolean: prompt += ' (y/n)' default = 'y' if default else 'n' @@ -213,6 +244,16 @@ def configure(self, prompt, default, parent, section_name): class ListAttribute(BaseValidated): """A config attribute containing a list of string values. + :param str name: the attribute name to use in the config file + :param strip: whether to strip whitespace from around each value (applies + only to legacy comma-separated lists; multi-line lists are + always stripped) + :type strip: bool, optional + :param default: the default value if the config file does not define a + value for this option; to require explicit configuration, + use :const:`sopel.config.types.NO_DEFAULT` + :type default: list, optional + From this :class:`StaticSection`:: class SpamSection(StaticSection): @@ -265,7 +306,7 @@ def parse(self, value): :param str value: a multi-line string of values to parse into a list :return: a list of items from ``value`` - :rtype: :class:`list` + :rtype: list .. versionchanged:: 7.0 @@ -273,7 +314,7 @@ def parse(self, value): when there is no newline in ``value``. When modified and saved to a file, items will be stored as a - multi-line string. + multi-line string (see :meth:`serialize`). """ if "\n" in value: items = [ @@ -295,7 +336,12 @@ def parse(self, value): return value def serialize(self, value): - """Serialize ``value`` into a multi-line string.""" + """Serialize ``value`` into a multi-line string. + + :param list value: the input list + :rtype: str + :raise ValueError: if ``value`` is the wrong type (i.e. not a list) + """ if not isinstance(value, (list, set)): raise ValueError('ListAttribute value must be a list.') @@ -305,6 +351,10 @@ def serialize(self, value): return '\n' + '\n'.join(value) def configure(self, prompt, default, parent, section_name): + """ + With the ``prompt`` and ``default``, parse and return a value from + terminal. + """ each_prompt = '?' if isinstance(prompt, tuple): each_prompt = prompt[1] @@ -332,18 +382,39 @@ def configure(self, prompt, default, parent, section_name): class ChoiceAttribute(BaseValidated): """A config attribute which must be one of a set group of options. - Currently, the choices can only be strings.""" + :param str name: the attribute name to use in the config file + :param choices: acceptable values; currently, only strings are supported + :type choices: list or tuple + :param default: which choice to use if none is set in the config file; to + require explicit configuration, use + :const:`sopel.config.types.NO_DEFAULT` + :type default: str, optional + """ def __init__(self, name, choices, default=None): super(ChoiceAttribute, self).__init__(name, default=default) self.choices = choices def parse(self, value): + """Check the loaded ``value`` against the valid ``choices``. + + :param str value: the value loaded from the config file + :return: the ``value``, if it is valid + :rtype: str + :raise ValueError: if ``value`` is not one of the valid ``choices`` + """ if value in self.choices: return value else: raise ValueError('Value must be in {}'.format(self.choices)) def serialize(self, value): + """Make sure ``value`` is valid and safe to write in the config file. + + :param str value: the value needing to be saved + :return: the ``value``, if it is valid + :rtype: str + :raise ValueError: if ``value`` is not one of the valid ``choices`` + """ if value in self.choices: return value else: @@ -351,14 +422,21 @@ def serialize(self, value): class FilenameAttribute(BaseValidated): - """A config attribute which must be a file or directory.""" + """A config attribute which must be a file or directory. + + :param str name: the attribute name to use in the config file + :param relative: whether the path should be relative to the location of + the config file (absolute paths will still be absolute) + :type relative: bool, optional + :param directory: whether the path should indicate a directory, rather + than a file + :type directory: bool, optional + :param default: the value to use if none is defined in the config file; to + require explicit configuration, use + :const:`sopel.config.types.NO_DEFAULT` + :type default: str, optional + """ def __init__(self, name, relative=True, directory=False, default=None): - """ - ``relative`` is whether the path should be relative to the location - of the config file (absolute paths will still be absolute). If - ``directory`` is True, the path must indicate a directory, rather than - a file. - """ super(FilenameAttribute, self).__init__(name, default=default) self.relative = relative self.directory = directory @@ -391,7 +469,9 @@ def __set__(self, instance, value): instance._parser.set(instance._section_name, self.name, value) def configure(self, prompt, default, parent, section_name): - """With the prompt and default, parse and return a value from terminal. + """ + With the ``prompt`` and ``default``, parse and return a value from + terminal. """ if default is not NO_DEFAULT and default is not None: prompt = '{} [{}]'.format(prompt, default) @@ -402,6 +482,16 @@ def configure(self, prompt, default, parent, section_name): return self.parse(parent, section_name, value) def parse(self, main_config, this_section, value): + """Used to validate ``value`` when loading the config. + + :param main_config: the config object which contains this attribute + :type main_config: :class:`~sopel.config.Config` + :param this_section: the config section which contains this attribute + :type this_section: :class:`~StaticSection` + :return: the ``value``, if it is valid + :rtype: str + :raise ValueError: if the ``value`` is not valid + """ if value is None: return @@ -426,5 +516,15 @@ def parse(self, main_config, this_section, value): return value def serialize(self, main_config, this_section, value): + """Used to validate ``value`` when it is changed at runtime. + + :param main_config: the config object which contains this attribute + :type main_config: :class:`~sopel.config.Config` + :param this_section: the config section which contains this attribute + :type this_section: :class:`~StaticSection` + :return: the ``value``, if it is valid + :rtype: str + :raise ValueError: if the ``value`` is not valid + """ self.parse(main_config, this_section, value) return value # So that it's still relative