From feb7ce11ccb677340d7f86038db1d8d38be11901 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Thu, 30 May 2019 16:34:26 +0200 Subject: [PATCH 1/6] test: use pytest instead of unittest test_config.py --- test/test_config.py | 212 +++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 99 deletions(-) diff --git a/test/test_config.py b/test/test_config.py index 653d7ea4a0..f0b49cdceb 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,12 +2,20 @@ from __future__ import unicode_literals, division, print_function, absolute_import import os -import tempfile -import unittest + +import pytest + from sopel import config from sopel.config import types +FAKE_CONFIG = """ +[core] +owner=dgw +homedir={homedir} +""" + + class FakeConfigSection(types.StaticSection): valattr = types.ValidatedAttribute('valattr') listattr = types.ListAttribute('listattr') @@ -18,131 +26,137 @@ class FakeConfigSection(types.StaticSection): rd_fileattr = types.FilenameAttribute('rd_fileattr', relative=True, directory=True) -class ConfigFunctionalTest(unittest.TestCase): - @classmethod - def read_config(cls): - configo = config.Config(cls.filename) - configo.define_section('fake', FakeConfigSection) - return configo +@pytest.fixture +def fakeconfig(tmpdir): + sopel_homedir = tmpdir.join('.sopel') + sopel_homedir.mkdir() + sopel_homedir.join('test.tmp').write('') + sopel_homedir.join('test.d').mkdir() + conf_file = sopel_homedir.join('conf.cfg') + conf_file.write(FAKE_CONFIG.format(homedir=sopel_homedir.strpath)) + + test_settings = config.Config(conf_file.strpath) + test_settings.define_section('fake', FakeConfigSection) + return test_settings + + +def test_validated_string_when_none(fakeconfig): + fakeconfig.fake.valattr = None + assert fakeconfig.fake.valattr is None + + +def test_listattribute_when_empty(fakeconfig): + fakeconfig.fake.listattr = [] + assert fakeconfig.fake.listattr == [] + + +def test_listattribute_with_one_value(fakeconfig): + fakeconfig.fake.listattr = ['foo'] + assert fakeconfig.fake.listattr == ['foo'] + + +def test_listattribute_with_multiple_values(fakeconfig): + fakeconfig.fake.listattr = ['egg', 'sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['egg', 'sausage', 'bacon'] + + +def test_listattribute_with_value_containing_comma(fakeconfig): + fakeconfig.fake.listattr = ['spam, egg, sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam, egg, sausage', 'bacon'] + + +def test_listattribute_with_value_containing_nonescape_backslash(fakeconfig): + fakeconfig.fake.listattr = ['spam', r'egg\sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg\sausage', 'bacon'] + + fakeconfig.fake.listattr = ['spam', r'egg\tacos', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg\tacos', 'bacon'] + - @classmethod - def setUpClass(cls): - cls.filename = tempfile.mkstemp()[1] - with open(cls.filename, 'w') as fileo: - fileo.write( - "[core]\n" - "owner=dgw\n" - "homedir={}".format(os.path.expanduser('~/.sopel')) - ) +def test_listattribute_with_value_containing_standard_escape_sequence(fakeconfig): + fakeconfig.fake.listattr = ['spam', 'egg\tsausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg\tsausage', 'bacon'] - cls.config = cls.read_config() + fakeconfig.fake.listattr = ['spam', 'egg\nsausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg\nsausage', 'bacon'] - cls.testfile = open(os.path.expanduser('~/.sopel/test.tmp'), 'w+').name - cls.testdir = os.path.expanduser('~/.sopel/test.d/') - os.mkdir(cls.testdir) + fakeconfig.fake.listattr = ['spam', 'egg\\sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg\\sausage', 'bacon'] - @classmethod - def tearDownClass(cls): - os.remove(cls.filename) - os.remove(cls.testfile) - os.rmdir(cls.testdir) - def test_validated_string_when_none(self): - self.config.fake.valattr = None - self.assertEqual(self.config.fake.valattr, None) +def test_listattribute_with_value_ending_in_special_chars(fakeconfig): + fakeconfig.fake.listattr = ['spam', 'egg', 'sausage\\', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage\\', 'bacon'] - def test_listattribute_when_empty(self): - self.config.fake.listattr = [] - self.assertEqual(self.config.fake.listattr, []) + fakeconfig.fake.listattr = ['spam', 'egg', 'sausage,', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage,', 'bacon'] - def test_listattribute_with_one_value(self): - self.config.fake.listattr = ['foo'] - self.assertEqual(self.config.fake.listattr, ['foo']) + fakeconfig.fake.listattr = ['spam', 'egg', 'sausage,,', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage,,', 'bacon'] - def test_listattribute_with_multiple_values(self): - self.config.fake.listattr = ['egg', 'sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['egg', 'sausage', 'bacon']) - def test_listattribute_with_value_containing_comma(self): - self.config.fake.listattr = ['spam, egg, sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam, egg, sausage', 'bacon']) +def test_listattribute_with_value_containing_adjacent_special_chars(fakeconfig): + fakeconfig.fake.listattr = ['spam', r'egg\,sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg\,sausage', 'bacon'] - def test_listattribute_with_value_containing_nonescape_backslash(self): - self.config.fake.listattr = ['spam', r'egg\sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg\sausage', 'bacon']) + fakeconfig.fake.listattr = ['spam', r'egg\,\sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg\,\sausage', 'bacon'] - self.config.fake.listattr = ['spam', r'egg\tacos', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg\tacos', 'bacon']) + fakeconfig.fake.listattr = ['spam', r'egg,\,sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg,\,sausage', 'bacon'] - def test_listattribute_with_value_containing_standard_escape_sequence(self): - self.config.fake.listattr = ['spam', 'egg\tsausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg\tsausage', 'bacon']) + fakeconfig.fake.listattr = ['spam', 'egg,,sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg,,sausage', 'bacon'] - self.config.fake.listattr = ['spam', 'egg\nsausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg\nsausage', 'bacon']) + fakeconfig.fake.listattr = ['spam', r'egg\\sausage', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', r'egg\\sausage', 'bacon'] - self.config.fake.listattr = ['spam', 'egg\\sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg\\sausage', 'bacon']) - def test_listattribute_with_value_ending_in_special_chars(self): - self.config.fake.listattr = ['spam', 'egg', 'sausage\\', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg', 'sausage\\', 'bacon']) +def test_choiceattribute_when_none(fakeconfig): + fakeconfig.fake.choiceattr = None + assert fakeconfig.fake.choiceattr is None - self.config.fake.listattr = ['spam', 'egg', 'sausage,', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg', 'sausage,', 'bacon']) - self.config.fake.listattr = ['spam', 'egg', 'sausage,,', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg', 'sausage,,', 'bacon']) +def test_choiceattribute_when_not_in_set(fakeconfig): + with pytest.raises(ValueError): + fakeconfig.fake.choiceattr = 'sausage' - def test_listattribute_with_value_containing_adjacent_special_chars(self): - self.config.fake.listattr = ['spam', r'egg\,sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg\,sausage', 'bacon']) - self.config.fake.listattr = ['spam', r'egg\,\sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg\,\sausage', 'bacon']) +def test_choiceattribute_when_valid(fakeconfig): + fakeconfig.fake.choiceattr = 'bacon' + assert fakeconfig.fake.choiceattr == 'bacon' - self.config.fake.listattr = ['spam', r'egg,\,sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg,\,sausage', 'bacon']) - self.config.fake.listattr = ['spam', 'egg,,sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', 'egg,,sausage', 'bacon']) +def test_fileattribute_valid_absolute_file_path(fakeconfig): + testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp') + fakeconfig.fake.af_fileattr = testfile + assert fakeconfig.fake.af_fileattr == testfile - self.config.fake.listattr = ['spam', r'egg\\sausage', 'bacon'] - self.assertEqual(self.config.fake.listattr, ['spam', r'egg\\sausage', 'bacon']) - def test_choiceattribute_when_none(self): - self.config.fake.choiceattr = None - self.assertEqual(self.config.fake.choiceattr, None) +def test_fileattribute_valid_absolute_dir_path(fakeconfig): + testdir = os.path.join(fakeconfig.core.homedir, 'test.d') + fakeconfig.fake.ad_fileattr = testdir + assert fakeconfig.fake.ad_fileattr == testdir - def test_choiceattribute_when_not_in_set(self): - with self.assertRaises(ValueError): - self.config.fake.choiceattr = 'sausage' - def test_choiceattribute_when_valid(self): - self.config.fake.choiceattr = 'bacon' - self.assertEqual(self.config.fake.choiceattr, 'bacon') +def test_fileattribute_given_relative_when_absolute(fakeconfig): + with pytest.raises(ValueError): + fakeconfig.fake.af_fileattr = '../testconfig.tmp' - def test_fileattribute_valid_absolute_file_path(self): - self.config.fake.af_fileattr = self.testfile - self.assertEqual(self.config.fake.af_fileattr, self.testfile) - def test_fileattribute_valid_absolute_dir_path(self): - testdir = self.testdir - self.config.fake.ad_fileattr = testdir - self.assertEqual(self.config.fake.ad_fileattr, testdir) +def test_fileattribute_given_absolute_when_relative(fakeconfig): + testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp') + fakeconfig.fake.rf_fileattr = testfile + assert fakeconfig.fake.rf_fileattr == testfile - def test_fileattribute_given_relative_when_absolute(self): - with self.assertRaises(ValueError): - self.config.fake.af_fileattr = '../testconfig.tmp' - def test_fileattribute_given_absolute_when_relative(self): - self.config.fake.rf_fileattr = self.testfile - self.assertEqual(self.config.fake.rf_fileattr, self.testfile) +def test_fileattribute_given_dir_when_file(fakeconfig): + testdir = os.path.join(fakeconfig.core.homedir, 'test.d') + with pytest.raises(ValueError): + fakeconfig.fake.af_fileattr = testdir - def test_fileattribute_given_dir_when_file(self): - with self.assertRaises(ValueError): - self.config.fake.af_fileattr = self.testdir - def test_fileattribute_given_file_when_dir(self): - with self.assertRaises(ValueError): - self.config.fake.ad_fileattr = self.testfile +def test_fileattribute_given_file_when_dir(fakeconfig): + testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp') + with pytest.raises(ValueError): + fakeconfig.fake.ad_fileattr = testfile From 68a3d3052a8617ba90892253c84eb8ae9b57a459 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Thu, 30 May 2019 18:48:28 +0200 Subject: [PATCH 2/6] config: ListAttribute uses newlines by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Sopel 6.x and prior version, multi-line values would be split by commas: option = one, two, three and a half, four would give that: ['one', 'two', 'three\nand a half', 'four'] Also, it isn't possible to have a comma inside a value. However, a fellow Sopelunker, in #1460, added an escape mechanism. It was a good idea that didn't account for what Python's ConfigParser could do for us. Indeed, it accepts, as a built-in behavior, to declare an option on multiple lines. Getting the value would then returns a string, strip from extra spaces, with values separated by newlines. Therefore, all I had to do here was to: * detect if there was a newline character in the value * if so, split by newlines * otherwise keep using the comma-separated approach * remove (sadly) the usage of escape character * add some tests * improve docstrings here and there (they were outdated) * make sure we properly serialize back the values and voilà! --- sopel/config/types.py | 95 +++++++++++++++++++---------- test/test_config.py | 135 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 196 insertions(+), 34 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index c2fb876817..11a7ad6ecf 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -211,12 +211,45 @@ def configure(self, prompt, default, parent, section_name): class ListAttribute(BaseValidated): """A config attribute containing a list of string values. - Values are saved to the file as a comma-separated list. It does not - currently support commas within items in the list. By default, the spaces - before and after each item are stripped; you can override this by passing - ``strip=False``.""" + From this :class:`StaticSection`:: - ESCAPE_CHARACTER = '\\' + class SpamSection(StaticSection): + cheese = ListAttribute('cheese') + + the option will be exposed as a Python :class:`list`:: + + >>> config.spam.cheese +  ['camembert', 'cheddar', 'reblochon'] + + which come from this configuration file:: + + [spam] + cheese = camembert + cheddar + reblochon + + .. versionchanged:: 7.0 + + The option's value will be split by breakline by default. In this + case, the ``strip`` parameter has no effect. + + See the :meth:`parse` method for more information. + + .. note:: + + **About**: backward compatibility with comma separated values. + + A :class:`ListAttribute` option allows to write, on a single line, + the values separated by commas. As of Sopel 7.x this behavior is + discouraged. It will be deprecated in Sopel 8.x, then removed in + Sopel 9.x. + + Bot's owners are encouraged to update their configuration to use + breaklines instead of commas. + + The comma delimiter fallback does not support commas within items in + the list. + """ DELIMITER = ',' def __init__(self, name, strip=True, default=None): @@ -225,22 +258,28 @@ def __init__(self, name, strip=True, default=None): self.strip = strip def parse(self, value): - items = [] - is_escape_on = False - current_token = [] - for char in value: - if not is_escape_on: - if char == ListAttribute.ESCAPE_CHARACTER: - is_escape_on = True - elif char == ListAttribute.DELIMITER: - items.append(''.join(current_token)) - current_token = [] - else: - current_token.append(char) - else: - current_token.append(char) - is_escape_on = False - items.append(''.join(current_token)) + """Parse ``value`` into a list. + + :param str value: a multi-line string of values to parse into a list + :return: a list of items from ``value`` + :rtype: :class:`list` + + .. versionchanged:: 7.0 + + The value is now split by breaklines, and fallback on comma + when there is no breakline delimiter in ``value``. + + When modified and save to a file, items will be stored as a + multi-line string. + """ + if "\n" in value: + items = value.splitlines() + else: + # this behavior will be: + # - Discouraged in Sopel 7.x (in the documentation) + # - Deprecated in Sopel 8.x + # - Removed from Sopel 9.x + items = value.split(self.DELIMITER) value = list(filter(None, items)) if self.strip: @@ -249,18 +288,14 @@ def parse(self, value): return value def serialize(self, value): + """Serialize ``value`` into a multi-line string.""" if not isinstance(value, (list, set)): raise ValueError('ListAttribute value must be a list.') - items = [] - for item in value: - current_token = [] - for char in item: - if char in [ListAttribute.ESCAPE_CHARACTER, ListAttribute.DELIMITER]: - current_token.append(ListAttribute.ESCAPE_CHARACTER) - current_token.append(char) - items.append(''.join(current_token)) - return ','.join(items) + # we ensure to read a breakline, even with one value list + # this way, comma will be ignored when the configuration file + # will be read again later + return '\n' + '\n'.join(value) def configure(self, prompt, default, parent, section_name): each_prompt = '?' diff --git a/test/test_config.py b/test/test_config.py index f0b49cdceb..c9a6e59552 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -16,6 +16,19 @@ """ +MULTILINE_CONFIG = FAKE_CONFIG + """ +[spam] +eggs = one, two, three, four, and a half +bacons = grilled + burn out + greasy, fat, and tasty +cheese = + cheddar + reblochon + camembert +""" # noqa (trailing whitespaces are intended) + + class FakeConfigSection(types.StaticSection): valattr = types.ValidatedAttribute('valattr') listattr = types.ListAttribute('listattr') @@ -26,17 +39,39 @@ class FakeConfigSection(types.StaticSection): rd_fileattr = types.FilenameAttribute('rd_fileattr', relative=True, directory=True) +class SpamSection(types.StaticSection): + eggs = types.ListAttribute('eggs') + bacons = types.ListAttribute('bacons', strip=False) + cheese = types.ListAttribute('cheese') + + @pytest.fixture -def fakeconfig(tmpdir): +def tmphomedir(tmpdir): sopel_homedir = tmpdir.join('.sopel') sopel_homedir.mkdir() sopel_homedir.join('test.tmp').write('') sopel_homedir.join('test.d').mkdir() - conf_file = sopel_homedir.join('conf.cfg') - conf_file.write(FAKE_CONFIG.format(homedir=sopel_homedir.strpath)) + return sopel_homedir + + +@pytest.fixture +def fakeconfig(tmphomedir): + conf_file = tmphomedir.join('conf.cfg') + conf_file.write(FAKE_CONFIG.format(homedir=tmphomedir.strpath)) + + test_settings = config.Config(conf_file.strpath) + test_settings.define_section('fake', FakeConfigSection) + return test_settings + + +@pytest.fixture +def multi_fakeconfig(tmphomedir): + conf_file = tmphomedir.join('conf.cfg') + conf_file.write(MULTILINE_CONFIG.format(homedir=tmphomedir.strpath)) test_settings = config.Config(conf_file.strpath) test_settings.define_section('fake', FakeConfigSection) + test_settings.define_section('spam', SpamSection) return test_settings @@ -78,7 +113,9 @@ def test_listattribute_with_value_containing_standard_escape_sequence(fakeconfig assert fakeconfig.fake.listattr == ['spam', 'egg\tsausage', 'bacon'] fakeconfig.fake.listattr = ['spam', 'egg\nsausage', 'bacon'] - assert fakeconfig.fake.listattr == ['spam', 'egg\nsausage', 'bacon'] + assert fakeconfig.fake.listattr == [ + 'spam', 'egg', 'sausage', 'bacon' + ], 'Line break are always converted to new item' fakeconfig.fake.listattr = ['spam', 'egg\\sausage', 'bacon'] assert fakeconfig.fake.listattr == ['spam', 'egg\\sausage', 'bacon'] @@ -160,3 +197,93 @@ def test_fileattribute_given_file_when_dir(fakeconfig): testfile = os.path.join(fakeconfig.core.homedir, 'test.tmp') with pytest.raises(ValueError): fakeconfig.fake.ad_fileattr = testfile + + +def test_configparser_multi_lines(multi_fakeconfig): + # spam + assert multi_fakeconfig.spam.eggs == [ + 'one', + 'two', + 'three', + 'four', + 'and a half', # no-breakline + comma + ], 'Comma separated line: "four" and "and a half" must be separated' + assert multi_fakeconfig.spam.bacons == [ + 'grilled', + 'burn out', + 'greasy, fat, and tasty', + ] + assert multi_fakeconfig.spam.cheese == [ + 'cheddar', + 'reblochon', + 'camembert', + ] + + +def test_save_unmodified_config(multi_fakeconfig): + """Assert type attributes are kept as they should be""" + multi_fakeconfig.save() + saved_config = config.Config(multi_fakeconfig.filename) + saved_config.define_section('fake', FakeConfigSection) + saved_config.define_section('spam', SpamSection) + + # core + assert saved_config.core.owner == 'dgw' + + # fake + assert saved_config.fake.valattr is None + assert saved_config.fake.listattr == [] + assert saved_config.fake.choiceattr is None + assert saved_config.fake.af_fileattr is None + assert saved_config.fake.ad_fileattr is None + assert saved_config.fake.rf_fileattr is None + assert saved_config.fake.rd_fileattr is None + + # spam + assert saved_config.spam.eggs == [ + 'one', + 'two', + 'three', + 'four', + 'and a half', # no-breakline + comma + ], 'Comma separated line: "four" and "and a half" must be separated' + assert saved_config.spam.bacons == [ + 'grilled', + 'burn out', + 'greasy, fat, and tasty', + ] + assert saved_config.spam.cheese == [ + 'cheddar', + 'reblochon', + 'camembert', + ] + + +def test_save_modified_config(multi_fakeconfig): + """Assert modified values are restored properly""" + multi_fakeconfig.fake.choiceattr = 'spam' + multi_fakeconfig.spam.eggs = [ + 'one', + 'two', + ] + multi_fakeconfig.spam.cheese = [ + 'camembert, reblochon, and cheddar', + ] + + multi_fakeconfig.save() + + with open(multi_fakeconfig.filename) as fd: + print(fd.read()) # used for debug purpose if an assert fails + + saved_config = config.Config(multi_fakeconfig.filename) + saved_config.define_section('fake', FakeConfigSection) + saved_config.define_section('spam', SpamSection) + + assert saved_config.fake.choiceattr == 'spam' + assert saved_config.spam.eggs == ['one', 'two'] + assert saved_config.spam.cheese == [ + 'camembert, reblochon, and cheddar', + ], ( + 'ListAttribute with one line only, with commas, must *not* be split ' + 'differently from what was expected, ie. into one (and only one) value' + ) From 1659660cd245a31675c9f9e4e320e11626e14a8b Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Mon, 3 Jun 2019 20:16:42 +0200 Subject: [PATCH 3/6] config: remove trailing comma when using newlines --- sopel/config/types.py | 8 ++++++-- test/test_config.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index 11a7ad6ecf..1303b0c519 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -273,7 +273,11 @@ def parse(self, value): multi-line string. """ if "\n" in value: - items = value.splitlines() + items = [ + # remove trailing comma + # because `value,\nother` is valid in Sopel 7.x + item.strip(self.DELIMITER).strip() + for item in value.splitlines()] else: # this behavior will be: # - Discouraged in Sopel 7.x (in the documentation) @@ -282,7 +286,7 @@ def parse(self, value): items = value.split(self.DELIMITER) value = list(filter(None, items)) - if self.strip: + if self.strip: # deprecate strip option in Sopel 8.x return [v.strip() for v in value] else: return value diff --git a/test/test_config.py b/test/test_config.py index c9a6e59552..34ade6820a 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -20,8 +20,8 @@ [spam] eggs = one, two, three, four, and a half bacons = grilled - burn out - greasy, fat, and tasty + burn out, + , greasy, fat, and tasty cheese = cheddar reblochon @@ -126,10 +126,10 @@ def test_listattribute_with_value_ending_in_special_chars(fakeconfig): assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage\\', 'bacon'] fakeconfig.fake.listattr = ['spam', 'egg', 'sausage,', 'bacon'] - assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage,', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage', 'bacon'] fakeconfig.fake.listattr = ['spam', 'egg', 'sausage,,', 'bacon'] - assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage,,', 'bacon'] + assert fakeconfig.fake.listattr == ['spam', 'egg', 'sausage', 'bacon'] def test_listattribute_with_value_containing_adjacent_special_chars(fakeconfig): From 522d400f8355e86c2fedc1adfb7aa160ed4f1fcd Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sat, 31 Aug 2019 14:50:53 +0200 Subject: [PATCH 4/6] config: fix cheeses and grammar Co-Authored-By: dgw --- sopel/config/types.py | 30 +++++++++++++++--------------- test/test_config.py | 18 +++++++++--------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index 1303b0c519..de23895bb6 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -214,38 +214,38 @@ class ListAttribute(BaseValidated): From this :class:`StaticSection`:: class SpamSection(StaticSection): - cheese = ListAttribute('cheese') + cheeses = ListAttribute('cheeses') the option will be exposed as a Python :class:`list`:: - >>> config.spam.cheese -  ['camembert', 'cheddar', 'reblochon'] + >>> config.spam.cheeses + ['camembert', 'cheddar', 'reblochon'] which come from this configuration file:: [spam] - cheese = camembert - cheddar - reblochon + cheeses = camembert + cheddar + reblochon .. versionchanged:: 7.0 - The option's value will be split by breakline by default. In this + The option's value will be split on newlines by default. In this case, the ``strip`` parameter has no effect. See the :meth:`parse` method for more information. .. note:: - **About**: backward compatibility with comma separated values. + **About:** backward compatibility with comma-separated values. A :class:`ListAttribute` option allows to write, on a single line, the values separated by commas. As of Sopel 7.x this behavior is discouraged. It will be deprecated in Sopel 8.x, then removed in Sopel 9.x. - Bot's owners are encouraged to update their configuration to use - breaklines instead of commas. + Bot owners are encouraged to update their configurations to use + newlines instead of commas. The comma delimiter fallback does not support commas within items in the list. @@ -266,10 +266,10 @@ def parse(self, value): .. versionchanged:: 7.0 - The value is now split by breaklines, and fallback on comma - when there is no breakline delimiter in ``value``. + The value is now split on newlines, with fallback to comma + when there is no newline in ``value``. - When modified and save to a file, items will be stored as a + When modified and saved to a file, items will be stored as a multi-line string. """ if "\n" in value: @@ -296,9 +296,9 @@ def serialize(self, value): if not isinstance(value, (list, set)): raise ValueError('ListAttribute value must be a list.') - # we ensure to read a breakline, even with one value list + # we ensure to read a newline, even with only one value in the list # this way, comma will be ignored when the configuration file - # will be read again later + # is read again later return '\n' + '\n'.join(value) def configure(self, prompt, default, parent, section_name): diff --git a/test/test_config.py b/test/test_config.py index 34ade6820a..c5c10907b0 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -22,7 +22,7 @@ bacons = grilled burn out, , greasy, fat, and tasty -cheese = +cheeses = cheddar reblochon camembert @@ -42,7 +42,7 @@ class FakeConfigSection(types.StaticSection): class SpamSection(types.StaticSection): eggs = types.ListAttribute('eggs') bacons = types.ListAttribute('bacons', strip=False) - cheese = types.ListAttribute('cheese') + cheeses = types.ListAttribute('cheeses') @pytest.fixture @@ -206,14 +206,14 @@ def test_configparser_multi_lines(multi_fakeconfig): 'two', 'three', 'four', - 'and a half', # no-breakline + comma + 'and a half', # no-newline + comma ], 'Comma separated line: "four" and "and a half" must be separated' assert multi_fakeconfig.spam.bacons == [ 'grilled', 'burn out', 'greasy, fat, and tasty', ] - assert multi_fakeconfig.spam.cheese == [ + assert multi_fakeconfig.spam.cheeses == [ 'cheddar', 'reblochon', 'camembert', @@ -245,14 +245,14 @@ def test_save_unmodified_config(multi_fakeconfig): 'two', 'three', 'four', - 'and a half', # no-breakline + comma + 'and a half', # no-newline + comma ], 'Comma separated line: "four" and "and a half" must be separated' assert saved_config.spam.bacons == [ 'grilled', 'burn out', 'greasy, fat, and tasty', ] - assert saved_config.spam.cheese == [ + assert saved_config.spam.cheeses == [ 'cheddar', 'reblochon', 'camembert', @@ -266,7 +266,7 @@ def test_save_modified_config(multi_fakeconfig): 'one', 'two', ] - multi_fakeconfig.spam.cheese = [ + multi_fakeconfig.spam.cheeses = [ 'camembert, reblochon, and cheddar', ] @@ -281,9 +281,9 @@ def test_save_modified_config(multi_fakeconfig): assert saved_config.fake.choiceattr == 'spam' assert saved_config.spam.eggs == ['one', 'two'] - assert saved_config.spam.cheese == [ + assert saved_config.spam.cheeses == [ 'camembert, reblochon, and cheddar', ], ( 'ListAttribute with one line only, with commas, must *not* be split ' - 'differently from what was expected, ie. into one (and only one) value' + 'differently from what was expected, i.e. into one (and only one) value' ) From 63ef2a0bdb9b1b52c97fa1f3570514ecde14eba3 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Sat, 31 Aug 2019 15:54:11 +0200 Subject: [PATCH 5/6] doc: update example of list attribute with newlines --- docs/source/configuration.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index ff9a820f5d..ca1a459983 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -173,7 +173,9 @@ join is configured by :attr:`~CoreSection.channels`: .. code-block:: ini [core] - channels = #sopel, #sopelunkers + channels = + #sopel + #sopelunkers It is possible to slow down the initial joining of channels using :attr:`~CoreSection.throttle_join`, for example if the IRC network kicks @@ -388,7 +390,9 @@ can put its name in the :attr:`~CoreSection.exclude` directive. Here, the .. code-block:: ini [core] - exclude = reload, meetbot + exclude = + reload + meetbot Alternatively, you can define a list of allowed plugins with :attr:`~CoreSection.enable`: plugins not in this list will be ignored. In this @@ -398,8 +402,13 @@ example, only the ``bugzilla`` and ``remind`` plugins are enabled (because .. code-block:: ini [core] - enable = bugzilla, remind, meetbot - exclude = reload, meetbot + enable = + bugzilla + remind + meetbot + exclude = + reload + meetbot To detect plugins from extra directories, use the :attr:`~CoreSection.extra` option. From 010141cbe7eccb591ccf1027ed8d27d644e1f2b2 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Mon, 2 Sep 2019 21:37:29 +0200 Subject: [PATCH 6/6] config: properly format example --- sopel/config/types.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index de23895bb6..f394905cea 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -221,12 +221,13 @@ class SpamSection(StaticSection): >>> config.spam.cheeses ['camembert', 'cheddar', 'reblochon'] - which come from this configuration file:: + which comes from this configuration file:: [spam] - cheeses = camembert - cheddar - reblochon + cheeses = + camembert + cheddar + reblochon .. versionchanged:: 7.0