@@ -243,3 +243,19 @@ class UnsupportedBundleSpecification(Exc
class CorruptedState(Exception):
"""error raised when a command is not able to read its state from file"""
+
+class InvalidKeyValueFile(Exception):
+ """error raised when the file can't be parsed as simple key-value file"""
+
+class InvalidKeyInFileException(Exception):
+ """error raised when invalid key is attempted to be written
+
+ This is used in simple key-value file implementation"""
+
+class InvalidValueInFileException(Exception):
+ """error raisesd when invalid value is attempted to be written
+
+ This is used in simple key-value file implementation"""
+
+class MissingRequiredKeyInFileException(Exception):
+ """error raised when simple key-value file misses a required key"""
@@ -1571,3 +1571,123 @@ class checkambigatclosing(closewrapbase)
def close(self):
self._origfh.close()
self._checkambig()
+
+class simplekeyvaluefile(object):
+ """A simple file with key=value lines
+
+ Keys must be alphanumerics and start with a letter, values might not
+ contain '\n' characters
+
+ >>> contents = {}
+ >>> class fileobj(object):
+ ... def __init__(self, name):
+ ... self.name = name
+ ... def __enter__(self):
+ ... return self
+ ... def __exit__(self, *args, **kwargs):
+ ... pass
+ ... def write(self, text):
+ ... contents[self.name] = text
+ ... def read(self):
+ ... return contents[self.name]
+ >>> class mockvfs(object):
+ ... def read(self, path):
+ ... return fileobj(path).read()
+ ... def readlines(self, path):
+ ... return fileobj(path).read().split('\\n')
+ ... def __call__(self, path, mode, atomictemp):
+ ... return fileobj(path)
+ >>> vfs = mockvfs()
+
+ Basic testing of whether simple key-value file works:
+ >>> d = {'key1': 'value1', 'Key2': 'value2'}
+ >>> simplekeyvaluefile(vfs, 'kvfile').write(d)
+ >>> print sorted(vfs.read('kvfile').split('\\n'))
+ ['', 'Key2=value2', 'key1=value1']
+
+ Testing of whether invalid keys are detected:
+ >>> d = {'0key1': 'value1', 'Key2': 'value2'}
+ >>> simplekeyvaluefile(vfs, 'kvfile').write(d)
+ Traceback (most recent call last):
+ ...
+ InvalidKeyInFileException: keys must start with a letter ...
+ >>> d = {'key1@': 'value1', 'Key2': 'value2'}
+ >>> simplekeyvaluefile(vfs, 'kvfile').write(d)
+ Traceback (most recent call last):
+ ...
+ InvalidKeyInFileException: invalid key name in a simple key-value file
+
+ Testing of whether invalid values are detected:
+ >>> d = {'key1': 'value1', 'Key2': 'value2\\n'}
+ >>> simplekeyvaluefile(vfs, 'kvfile').write(d)
+ Traceback (most recent call last):
+ ...
+ InvalidValueInFileException: invalid value in a simple key-value file
+
+ Test cases when necessary keys are present
+ >>> d = {'key1': 'value1', 'Key2': 'value2'}
+ >>> simplekeyvaluefile(vfs, 'allkeyshere').write(d)
+ >>> class kvf(simplekeyvaluefile):
+ ... KEYS = [('key3', False), ('Key2', True)]
+ >>> print sorted(kvf(vfs, 'allkeyshere').read().items())
+ [('Key2', 'value'), ('key1', 'value')]
+
+ Test cases when necessary keys are absent
+ >>> d = {'key1': 'value1', 'Key3': 'value2'}
+ >>> simplekeyvaluefile(vfs, 'missingkeys').write(d)
+ >>> class kvf(simplekeyvaluefile):
+ ... KEYS = [('key3', False), ('Key2', True)]
+ >>> print sorted(kvf(vfs, 'missingkeys').read().items())
+ Traceback (most recent call last):
+ ...
+ MissingRequiredKeyInFileException: missing a required key: 'Key2'
+
+ Test cases when file is not really a simple key-value file
+ >>> contents['badfile'] = 'ababagalamaga\\n'
+ >>> simplekeyvaluefile(vfs, 'badfile').read()
+ Traceback (most recent call last):
+ ...
+ InvalidKeyValueFile: dictionary ... element #0 has length 1; 2 is required
+ """
+
+ # if KEYS is non-empty, read values are validated against it:
+ # each key is a tuple (keyname, required)
+ KEYS = []
+
+ def __init__(self, vfs, path):
+ self.vfs = vfs
+ self.path = path
+
+ def validate(self, d):
+ for key, req in self.KEYS:
+ if req and key not in d:
+ e = "missing a required key: '%s'" % key
+ raise error.MissingRequiredKeyInFileException(e)
+
+ 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.InvalidKeyValueFile(str(e))
+ self.validate(d)
+ return d
+
+ def write(self, data):
+ """Write key=>value mapping to a file
+ data is a dict. Keys should be alphanumerical and start with a letter.
+ Values should 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.InvalidKeyInFileException(e)
+ if not k.isalnum():
+ e = "invalid key name in a simple key-value file"
+ raise error.InvalidKeyInFileException(e)
+ if '\n' in v:
+ e = "invalid value in a simple key-value file"
+ raise error.InvalidValueInFileException(e)
+ lines.append("%s=%s\n" % (k, v))
+ with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
+ fp.write(''.join(lines))
@@ -28,6 +28,7 @@ testmod('mercurial.patch')
testmod('mercurial.pathutil')
testmod('mercurial.parser')
testmod('mercurial.revset')
+testmod('mercurial.scmutil', optionflags=doctest.ELLIPSIS)
testmod('mercurial.store')
testmod('mercurial.subrepo')
testmod('mercurial.templatefilters')