diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py --- a/mercurial/scmutil.py +++ b/mercurial/scmutil.py @@ -965,3 +965,41 @@ def gddeltaconfig(ui): """ # experimental config: format.generaldelta return ui.configbool('format', 'generaldelta', False) + +class simplekeyvaluefile(object): + """A simple file with key=value lines + + Keys must be alphanumerics and start with a letter, values must not + contain '\n' characters""" + + def __init__(self, vfs, path, keys=None): + self.vfs = vfs + self.path = path + + def read(self): + lines = self.vfs.readlines(self.path) + try: + d = dict(line[:-1].split('=', 1) for line in lines if line) + except ValueError as e: + raise error.CorruptedState(str(e)) + return d + + def write(self, data): + """Write key=>value mapping to a file + data is a dict. Keys must be alphanumerical and start with a letter. + Values must not contain newline characters.""" + lines = [] + for k, v in data.items(): + if not k[0].isalpha(): + e = "keys must start with a letter in a key-value file" + raise error.ProgrammingError(e) + if not k.isalnum(): + e = "invalid key name in a simple key-value file" + raise error.ProgrammingError(e) + if '\n' in v: + e = "invalid value in a simple key-value file" + raise error.ProgrammingError(e) + lines.append("%s=%s\n" % (k, v)) + with self.vfs(self.path, mode='wb', atomictemp=True) as fp: + fp.write(''.join(lines)) + diff --git a/tests/test-simplekeyvaluefile.py b/tests/test-simplekeyvaluefile.py new file mode 100644 --- /dev/null +++ b/tests/test-simplekeyvaluefile.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import + +import unittest +import silenttestrunner + +from mercurial import ( + error, + scmutil, +) + +class mockfile(object): + def __init__(self, name, fs): + self.name = name + self.fs = fs + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + pass + + def write(self, text): + self.fs.contents[self.name] = text + + def read(self): + return self.fs.contents[self.name] + +class mockvfs(object): + def __init__(self): + self.contents = {} + + def read(self, path): + return mockfile(path, self).read() + + def readlines(self, path): + return mockfile(path, self).read().split('\n') + + def __call__(self, path, mode, atomictemp): + return mockfile(path, self) + +class testsimplekeyvaluefile(unittest.TestCase): + def setUp(self): + self.vfs = mockvfs() + + def testbasicwriting(self): + d = {'key1': 'value1', 'Key2': 'value2'} + scmutil.simplekeyvaluefile(self.vfs, 'kvfile').write(d) + self.assertEqual(sorted(self.vfs.read('kvfile').split('\n')), + ['', 'Key2=value2', 'key1=value1']) + + def testinvalidkeys(self): + d = {'0key1': 'value1', 'Key2': 'value2'} + with self.assertRaisesRegexp(error.ProgrammingError, + "keys must start with a letter.*"): + scmutil.simplekeyvaluefile(self.vfs, 'kvfile').write(d) + d = {'key1@': 'value1', 'Key2': 'value2'} + with self.assertRaisesRegexp(error.ProgrammingError, "invalid key.*"): + scmutil.simplekeyvaluefile(self.vfs, 'kvfile').write(d) + + def testinvalidvalues(self): + d = {'key1': 'value1', 'Key2': 'value2\n'} + with self.assertRaisesRegexp(error.ProgrammingError, "invalid val.*"): + scmutil.simplekeyvaluefile(self.vfs, 'kvfile').write(d) + + def testcorruptedfile(self): + self.vfs.contents['badfile'] = 'ababagalamaga\n' + with self.assertRaisesRegexp(error.CorruptedState, + "dictionary.*element.*"): + scmutil.simplekeyvaluefile(self.vfs, 'badfile').read() + +if __name__ == "__main__": + silenttestrunner.main(__name__)