Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Unreleased

* raise ``InterpolationError`` instead of leaking a raw ``TypeError`` when a
value interpolates a reference to an option whose value is a list
* quote values on writing with ``list_values=False`` when they contain ``#``
or start with a quote character, and unquote them again on reading, so
written output can be loaded back unchanged (issue #270)

Release 5.0.9
"""""""""""""
Expand Down
29 changes: 23 additions & 6 deletions src/configobj/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,8 @@ def _quote(self, value, multiline=True):
* Obey list syntax for empty and single member lists.

If ``list_values=False`` then the value is only quoted if it contains
a ``\\n`` (is multiline) or '#'.
a ``\\n`` (is multiline), starts with a quote character, or contains
'#', so that written output can be read back unchanged.

If ``write_empty_values`` is set, and the value is an empty string, it
won't be quoted.
Expand Down Expand Up @@ -1737,15 +1738,23 @@ def _quote(self, value, multiline=True):
if not value:
return '""'

no_lists_no_quotes = not self.list_values and '\n' not in value and '#' not in value
no_lists_no_quotes = (not self.list_values and '\n' not in value
and '#' not in value and value[0] not in ('"', "'"))
need_triple = multiline and ((("'" in value) and ('"' in value)) or ('\n' in value ))
hash_triple_quote = multiline and not need_triple and ("'" in value) and ('"' in value) and ('#' in value)
check_for_single = (no_lists_no_quotes or not need_triple) and not hash_triple_quote

if check_for_single:
if not self.list_values:
# we don't quote if ``list_values=False``
quot = noquot
if no_lists_no_quotes:
# we don't quote if ``list_values=False``
# and the bare form cannot be misparsed when read back
quot = noquot
else:
# a ``#`` would truncate the value at reading time, and
# a leading quote character makes the line unparseable,
# so quote to keep the output re-parseable
quot = self._get_single_quote(value)
# for normal values either single or double quotes will do
elif '\n' in value:
# will only happen if multiline is off - e.g. '\n' in key
Expand Down Expand Up @@ -1799,8 +1808,16 @@ def _handle_value(self, value):
mat = self._nolistvalue.match(value)
if mat is None:
raise SyntaxError()
# NOTE: we don't unquote here
return mat.groups()
entry = mat.group(1)
comment = mat.group(2)
if (len(entry) >= 2 and entry[0] == entry[-1]
and entry[0] in ('"', "'")
and entry[0] not in entry[1:-1]):
# a fully quoted value is unquoted here, symmetrically to
# how ``_quote`` writes it with ``list_values=False``;
# anything else keeps its verbatim text
entry = self._unquote(entry)
return (entry, comment)
#
mat = self._valueexp.match(value)
if mat is None:
Expand Down
27 changes: 24 additions & 3 deletions src/tests/test_configobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ def test_behavior_when_list_values_is_false():
cfg = ConfigObj(cfg_lines(c), list_values=False)
assert cfg == {
'key1': 'no quotes',
'key2': "'single quotes'",
'key3': '"double quotes"',
'key2': 'single quotes',
'key3': 'double quotes',
'key4': '"list", \'with\', several, "quotes"'
}

Expand All @@ -404,7 +404,7 @@ def test_behavior_when_list_values_is_false():
cfg2['key2'] = '''"Value" with 'quotes' !'''
assert cfg2.write() == [
"key1 = '''Multiline\nValue'''",
'key2 = "Value" with \'quotes\' !'
'key2 = \'\'\'"Value" with \'quotes\' !\'\'\''
]

cfg2.list_values = True
Expand All @@ -414,6 +414,27 @@ def test_behavior_when_list_values_is_false():
]


def test_list_values_false_written_output_can_be_read_back():
# A ``#`` would truncate the value at reading time and a leading quote
# character would make the line unparseable, so such values are quoted
# on writing and unquoted on reading again.
values = [
'hello # world',
'"hello" world',
"'quoted' # with hash",
'',
'no special characters',
]
for value in values:
cfg = ConfigObj(list_values=False)
cfg['key'] = value
assert ConfigObj(cfg.write(), list_values=False)['key'] == value

cfg = ConfigObj(list_values=False)
cfg['key'] = 'hello # world'
assert cfg.write() == ['key = "hello # world"']


def test_flatten_errors(val, cfg_contents):
config = cfg_contents("""
test1=40
Expand Down