# HG changeset patch
# User Kostia Balytskyi <ikos...@fb.com>
# Date 1480978778 28800
#      Mon Dec 05 14:59:38 2016 -0800
# Node ID bdc49209b0b926214e865f9a98ea987866f64d68
# Parent  243ecbd4f5c9f452275d4435866359cf84dc03ff
scmutil: add a simple key-value file helper

The purpose of the added class is to serve purposes like save files of shelve
or state files of shelve, rebase and histedit. Keys of these files can be
alphanumeric and start with letters, while values must not contain newlines.
Keys which start with an uppercase letter are required, while other keys
are optional.

In light of Mercurial's reluctancy to use Python's json module, this tries
to provide a reasonable alternative for a non-nested named data.
Comparing to current approach of storing state in plain text files, where
semantic meaning of lines of text is only determined by their oreder,
simple key-value file allows for reordering lines and thus helps handle
optional values.

Initial use-case I see for this is obs-shelve's shelve files. Later we
can possibly migrate state files to this approach.

The test is in a new file beause I did not figure out where to put it
within existing test suite. If you give me a better idea, I will gladly
follow it.

diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -1571,3 +1571,56 @@ 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
+    """
+    class InvalidKeyValueFile(Exception): pass
+    class InvalidKeyInFileException(Exception): pass
+    class InvalidValueInFileException(Exception): pass
+    class MissingRequiredKeyInFileException(Exception): pass
+
+    # 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 self.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 self.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 self.InvalidKeyInFileException(e)
+            if not k.isalnum():
+                e = _("invalid key name in a simple key-value file")
+                raise self.InvalidKeyInFileException(e)
+            if '\n' in v:
+                e = _("invalid value in a simple key-value file")
+                raise self.InvalidValueInFileException(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-other.t b/tests/test-other.t
new file mode 100644
--- /dev/null
+++ b/tests/test-other.t
@@ -0,0 +1,102 @@
+Test simple key-value files
+  $ cd $TESTTMP
+  $ hg init repo
+  $ cd $TESTTMP/repo
+
+Test simple key-value file creation
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key1': 'value1', 'Key2': 'value2'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py
+  $ cat .hg/kvfile | sort
+  Key2=value2
+  key1=value1
+
+Test simple key-value file reading with invalid keys or values
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'0key1': 'value1', 'Key2': 'value2'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyInFileException: keys must start with a letter 
in a key-value file
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key@1': 'value1'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyInFileException: invalid key name in a simple 
key-value file
+  $ cat <<EOF > keyvalwriter.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = {'key1': 'value\n1'}
+  > kvf = simplekeyvaluefile(repo.vfs, 'kvfile').write(d)
+  > EOF
+  $ python keyvalwriter.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidValueInFileException: invalid value in a simple 
key-value file
+
+Test simple key-value file reading without field list
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > d = simplekeyvaluefile(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py
+  Key2 => value2
+  key1 => value1
+
+Test simple key-value file when necessary fields are present
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > class kvf(simplekeyvaluefile):
+  >     KEYS = [('key3', False), ('Key2', True)]
+  > d = kvf(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py
+  Key2 => value2
+  key1 => value1
+
+Test simple key-value file when necessary fields are absent
+  $ cat <<EOF > keyvalreader.py
+  > from mercurial import ui, hg
+  > from mercurial.scmutil import simplekeyvaluefile
+  > ui = ui.ui()
+  > repo = hg.repository(ui, '$TESTTMP/repo')
+  > class kvf(simplekeyvaluefile):
+  >     KEYS = [('Key4', True), ('Key2', True)]
+  > d = kvf(repo.vfs, 'kvfile').read()
+  > for k, v in sorted(d.items()):
+  >     print "%s => %s" % (k, v)
+  > EOF
+  $ python keyvalreader.py 2>&1 | tail -1
+  mercurial.scmutil.MissingRequiredKeyInFileException: missing a required key: 
'Key4'
+
+Test invalid simple key-value file
+  $ cat <<EOF > .hg/kvfile
+  > ababagalamaga
+  > EOF
+  $ python keyvalreader.py 2>&1 | tail -1
+  mercurial.scmutil.InvalidKeyValueFile: dictionary update sequence element #0 
has length 1; 2 is required
_______________________________________________
Mercurial-devel mailing list
Mercurial-devel@mercurial-scm.org
https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel

Reply via email to