[PATCH RFC v2] scmutil: add a simple key-value file helper

Kostia Balytskyi ikostia at fb.com
Mon Dec 5 15:02:36 EST 2016


# HG changeset patch
# User Kostia Balytskyi <ikostia at fb.com>
# Date 1480967781 28800
#      Mon Dec 05 11:56:21 2016 -0800
# Node ID eceaa3ab833141da00291a236a20ca0be0b4b3b0
# 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,55 @@ class checkambigatclosing(closewrapbase)
     def close(self):
         self._origfh.close()
         self._checkambig()
+
+class simplekeyvaluefile(object):
+    """A simple file with key=value lines
+
+    Kyes 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:
+    # - if field name starts with an uppercase letter, this FIELDS
+    #   is considered required and its absense is an Exception
+    # - if field name starts with a lowercase letter, it is optional
+    #   and serves mainly as a reference for code reader
+    KEYS = []
+
+    def __init__(self, vfs, path):
+        self.vfs = vfs
+        self.path = path
+
+    def validate(self, d):
+        for k in self.KEYS:
+            if k[0].isupper() and k not in d:
+                e = _("Missing a required key: '%s'" % k)
+                raise self.MissingRequiredKeyInFileException(e)
+
+    def read(self):
+        lines = self.vfs.readlines(self.path)
+        try:
+            d = dict(line[:-1].split('=', 1) for line in lines)
+        except ValueError as e:
+            raise self.InvalidKeyValueFile(str(e))
+        self.validate(d)
+        return d
+
+    def write(self, data):
+        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))
+        self.vfs.writelines(self.path, 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 at 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', 'Key2']
+  > 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', 'Key2']
+  > 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


More information about the Mercurial-devel mailing list