config: Add preprocessor for preserving comments when editing configs.

Adds markers to configs files that ensures configparser won't mangle comments
in the files. Will be combined with a postprocessor that undoes these changes.
This commit is contained in:
Thomas Adamcik 2013-10-27 11:38:01 +01:00
parent add79d90dd
commit d5cb4282d9
2 changed files with 133 additions and 0 deletions

View File

@ -2,8 +2,10 @@ from __future__ import unicode_literals
import ConfigParser as configparser import ConfigParser as configparser
import io import io
import itertools
import logging import logging
import os.path import os.path
import re
from mopidy.config import keyring from mopidy.config import keyring
from mopidy.config.schemas import * # noqa from mopidy.config.schemas import * # noqa
@ -145,6 +147,41 @@ def _format(config, comments, schemas, display):
return b'\n'.join(output) return b'\n'.join(output)
def _preprocess(string):
"""Convert a raw config into a form that preserves comments etc."""
results = ['[__COMMENTS__]']
counter = itertools.count(0)
section_re = re.compile(r'^(\[[^\]]+\])\s*(.+)$')
blank_line_re = re.compile(r'^\s*$')
comment_re = re.compile(r'^(#|;)')
inline_comment_re = re.compile(r' ;')
def newlines(match):
return '__BLANK%d__ =' % next(counter)
def comments(match):
if match.group(1) == '#':
return '__HASH%d__ =' % next(counter)
elif match.group(1) == ';':
return '__SEMICOLON%d__ =' % next(counter)
def inlinecomments(match):
return '\n__INLINE%d__ =' % next(counter)
def sections(match):
return '%s\n__SECTION%d__ = %s' % (
match.group(1), next(counter), match.group(2))
for line in string.splitlines():
line = blank_line_re.sub(newlines, line)
line = section_re.sub(sections, line)
line = comment_re.sub(comments, line)
line = inline_comment_re.sub(inlinecomments, line)
results.append(line)
return '\n'.join(results)
class Proxy(collections.Mapping): class Proxy(collections.Mapping):
def __init__(self, data): def __init__(self, data):
self._data = data self._data = data

View File

@ -106,3 +106,99 @@ class ValidateTest(unittest.TestCase):
self.assertEqual({'foo': {'bar': 'bad'}}, errors) self.assertEqual({'foo': {'bar': 'bad'}}, errors)
# TODO: add more tests # TODO: add more tests
INPUT_CONFIG = """# comments before first section should work
[section] anything goes ; after the [] block it seems.
; this is a valid comment
this-should-equal-baz = baz ; as this is a comment
this-should-equal-everything = baz # as this is not a comment
# this is also a comment ; and the next line should be a blank comment.
;
# foo # = should all be treated as a comment.
"""
PROCESSED_CONFIG = """[__COMMENTS__]
__HASH0__ = comments before first section should work
__BLANK1__ =
[section]
__SECTION2__ = anything goes
__INLINE3__ = after the [] block it seems.
__SEMICOLON4__ = this is a valid comment
this-should-equal-baz = baz
__INLINE5__ = as this is a comment
this-should-equal-everything = baz # as this is not a comment
__BLANK6__ =
__HASH7__ = this is also a comment
__INLINE8__ = and the next line should be a blank comment.
__SEMICOLON9__ =
__HASH10__ = foo # = should all be treated as a comment."""
class ProcessorTest(unittest.TestCase):
maxDiff = None # Show entire diff.
def test_preprocessor_empty_config(self):
result = config._preprocess('')
self.assertEqual(result, '[__COMMENTS__]')
def test_preprocessor_plain_section(self):
result = config._preprocess('[section]\nfoo = bar')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar')
def test_preprocessor_initial_comments(self):
result = config._preprocess('; foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foobar')
result = config._preprocess('# foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__HASH0__ = foobar')
result = config._preprocess('; foo\n# bar')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__HASH1__ = bar')
def test_preprocessor_initial_comment_inline_handling(self):
result = config._preprocess('; foo ; bar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'__SEMICOLON0__ = foo\n'
'__INLINE1__ = bar\n'
'__INLINE2__ = baz')
def test_preprocessor_inline_semicolon_comment(self):
result = config._preprocess('[section]\nfoo = bar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar\n'
'__INLINE0__ = baz')
def test_preprocessor_no_inline_hash_comment(self):
result = config._preprocess('[section]\nfoo = bar # baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'foo = bar # baz')
def test_preprocessor_section_extra_text(self):
result = config._preprocess('[section] foobar')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar')
def test_preprocessor_section_extra_text_inline_semicolon(self):
result = config._preprocess('[section] foobar ; baz')
self.assertEqual(result, '[__COMMENTS__]\n'
'[section]\n'
'__SECTION0__ = foobar\n'
'__INLINE1__ = baz')
def test_preprocessor_conversion(self):
"""Tests all of the above cases at once."""
result = config._preprocess(INPUT_CONFIG)
self.assertEqual(result, PROCESSED_CONFIG)