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:
parent
add79d90dd
commit
d5cb4282d9
@ -2,8 +2,10 @@ from __future__ import unicode_literals
|
||||
|
||||
import ConfigParser as configparser
|
||||
import io
|
||||
import itertools
|
||||
import logging
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from mopidy.config import keyring
|
||||
from mopidy.config.schemas import * # noqa
|
||||
@ -145,6 +147,41 @@ def _format(config, comments, schemas, display):
|
||||
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):
|
||||
def __init__(self, data):
|
||||
self._data = data
|
||||
|
||||
@ -106,3 +106,99 @@ class ValidateTest(unittest.TestCase):
|
||||
self.assertEqual({'foo': {'bar': 'bad'}}, errors)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user