From c2623902a0803ef9cabedfaeee0a17c1a6d66527 Mon Sep 17 00:00:00 2001 From: Humorous Baby <44451911+HumorBaby@users.noreply.github.com> Date: Sat, 26 Jan 2019 20:47:31 -0500 Subject: [PATCH] config: support comma in config `ListAttribute` item This is a first step towards completing #1455, since, as @dgw mentioned, to do so would require accepting commas in a single entry of `ListAttribute`. The following tests are now added/updated for "`ListAttribute` items...": - ... containing comma(s) - ... with a non-escape backslash - ... with escape sequences - ... ending in special characters (escape character and delimiter) - ... containing adjacent special characters --- sopel/config/types.py | 29 ++++++++++++++++++++++++++-- test/test_config.py | 45 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/sopel/config/types.py b/sopel/config/types.py index 2da4fe2f9c..efa314d9ff 100644 --- a/sopel/config/types.py +++ b/sopel/config/types.py @@ -212,13 +212,34 @@ class ListAttribute(BaseValidated): 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``.""" + + ESCAPE_CHARACTER = '\\' + DELIMITER = ',' + def __init__(self, name, strip=True, default=None): default = default or [] super(ListAttribute, self).__init__(name, default=default) self.strip = strip def parse(self, value): - value = list(filter(None, value.split(','))) + 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)) + + value = list(filter(None, items)) if self.strip: return [v.strip() for v in value] else: @@ -227,7 +248,11 @@ def parse(self, value): def serialize(self, value): if not isinstance(value, (list, set)): raise ValueError('ListAttribute value must be a list.') - return ','.join(value) + + items = value + for special_char in [ListAttribute.ESCAPE_CHARACTER, ListAttribute.DELIMITER]: + items = map(lambda item: item.replace(special_char, '{}{}'.format(ListAttribute.ESCAPE_CHARACTER, special_char)), items) + return ','.join(items) def configure(self, prompt, default, parent, section_name): each_prompt = '?' diff --git a/test/test_config.py b/test/test_config.py index 73d4d286f4..653d7ea4a0 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -65,7 +65,50 @@ def test_listattribute_with_multiple_values(self): 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']) + self.assertEqual(self.config.fake.listattr, ['spam, 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']) + + self.config.fake.listattr = ['spam', r'egg\tacos', 'bacon'] + self.assertEqual(self.config.fake.listattr, ['spam', r'egg\tacos', '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']) + + self.config.fake.listattr = ['spam', 'egg\nsausage', 'bacon'] + self.assertEqual(self.config.fake.listattr, ['spam', 'egg\nsausage', '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']) + + 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_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']) + + 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']) + + 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