From c16c905143a1efaf5c69228511d54c7cda7d0f0c Mon Sep 17 00:00:00 2001
From: Vincent Pelletier <vincent@nexedi.com>
Date: Wed, 18 Apr 2012 09:54:36 +0200
Subject: [PATCH] [feat] Add support for a few built-in python object types as
 values

Useful when recipes generate non-string values to be reused by other
recipes.
---
 src/zc/buildout/buildout.py | 92 +++++++++++++++++++++++++++++++++----
 1 file changed, 84 insertions(+), 8 deletions(-)

diff --git a/src/zc/buildout/buildout.py b/src/zc/buildout/buildout.py
index 298f99d9..a853a7cf 100644
--- a/src/zc/buildout/buildout.py
+++ b/src/zc/buildout/buildout.py
@@ -56,6 +56,7 @@ import shutil
 import subprocess
 import sys
 import tempfile
+import pprint
 import zc.buildout
 import zc.buildout.download
 from functools import partial
@@ -94,6 +95,66 @@ def print_(*args, **kw):
         file = sys.stdout
     file.write(sep.join(map(str, args))+end)
 
+_MARKER = []
+
+class BuildoutSerialiser(object):
+    # XXX: I would like to access pprint._safe_repr, but it's not
+    # officially available. PrettyPrinter class has a functionally-speaking
+    # static method "format" which just calls _safe_repr, but it is not
+    # declared as static... So I must create an instance of it.
+    _format = pprint.PrettyPrinter().format
+    _dollar = '\\x%02x' % ord('$')
+    _semicolon = '\\x%02x' % ord(';')
+    _safe_globals = {'__builtins__': {
+        # Types which are represented as calls to their constructor.
+        'bytearray': bytearray,
+        'complex': complex,
+        'frozenset': frozenset,
+        'set': set,
+        # Those buildins are available through keywords, which allow creating
+        # instances which in turn give back access to classes. So no point in
+        # hiding them.
+        'dict': dict,
+        'list': list,
+        'str': str,
+        'tuple': tuple,
+        'False': False,
+        'True': True,
+        'None': None,
+    }}
+
+    def loads(self, value):
+        return eval(value, self._safe_globals)
+
+    def dumps(self, value):
+        value, isreadable, _ = self._format(value, {}, 0, 0)
+        if not isreadable:
+            raise ValueError('Value cannot be serialised: %s' % (value, ))
+        return value.replace('$', self._dollar).replace(';', self._semicolon)
+
+SERIALISED_VALUE_MAGIC = '!py'
+SERIALISED = re.compile(SERIALISED_VALUE_MAGIC + '([^!]*)!(.*)')
+SERIALISER_REGISTRY = {
+    '': BuildoutSerialiser(),
+}
+SERIALISER_VERSION = ''
+SERIALISER = SERIALISER_REGISTRY[SERIALISER_VERSION]
+# Used only to compose data
+SERIALISER_PREFIX = SERIALISED_VALUE_MAGIC + SERIALISER_VERSION + '!'
+assert SERIALISED.match(SERIALISER_PREFIX).groups() == (
+    SERIALISER_VERSION, ''), SERIALISED.match(SERIALISER_PREFIX).groups()
+
+def dumps(value):
+    orig_value = value
+    value = SERIALISER.dumps(value)
+    assert SERIALISER.loads(value) == orig_value, (repr(value), orig_value)
+    return SERIALISER_PREFIX + value
+
+def loads(value):
+    assert value.startswith(SERIALISED_VALUE_MAGIC), repr(value)
+    version, data = SERIALISED.match(value).groups()
+    return SERIALISER_REGISTRY[version].loads(data)
+
 realpath = zc.buildout.easy_install.realpath
 
 _isurl = re.compile('([a-zA-Z0-9+.-]+)://').match
@@ -1543,7 +1604,17 @@ class Options(DictMixin):
                        for s in v.split('$$')])
         self._cooked[option] = v
 
-    def get(self, option, default=None, seen=None, last=True):
+    def get(self, *args, **kw):
+      v = self._get(*args, **kw)
+      if hasattr(v, 'startswith') and v.startswith(SERIALISED_VALUE_MAGIC):
+        v = loads(v)
+      return v
+
+    def _get(self, option, default=None, seen=None, last=True):
+        # TODO: raise instead of handling a default parameter,
+        #       so that get() never tries to deserialize a default value
+        #       (and then: move deserialization to __getitem__
+        #                  and make get() use __getitem__)
         try:
             if last:
                 return self._data[option].replace('$${', '${')
@@ -1628,7 +1699,7 @@ class Options(DictMixin):
                     options = self.buildout[section]
                 finally:
                     del self.buildout._initializing[-1]
-            v = options.get(option, None, seen, last=last)
+            v = options._get(option, None, seen, last=last)
             if v is None:
                 if option == '_buildout_section_name_':
                     v = self.name
@@ -1641,14 +1712,14 @@ class Options(DictMixin):
         return ''.join([''.join(v) for v in zip(value[::2], subs)])
 
     def __getitem__(self, key):
-        v = self.get(key)
-        if v is None:
+        v = self.get(key, _MARKER)
+        if v is _MARKER:
             raise MissingOption("Missing option: %s:%s" % (self.name, key))
         return v
 
     def __setitem__(self, option, value):
         if not isinstance(value, str):
-            raise TypeError('Option values must be strings', value)
+            value = dumps(value)
         self._data[option] = value
 
     def __delitem__(self, key):
@@ -1677,6 +1748,9 @@ class Options(DictMixin):
         result = copy.deepcopy(self._raw)
         result.update(self._cooked)
         result.update(self._data)
+        for key, value in result.items():
+            if value.startswith(SERIALISED_VALUE_MAGIC):
+                result[key] = loads(value)
         return result
 
     def _call(self, f):
@@ -1749,6 +1823,8 @@ def _quote_spacey_nl(match):
     return result
 
 def _save_option(option, value, f):
+    if not isinstance(value, str):
+        value = dumps(value)
     value = _spacey_nl.sub(_quote_spacey_nl, value)
     if value.startswith('\n\t'):
         value = '%(__buildout_space_n__)s' + value[2:]
@@ -1758,9 +1834,9 @@ def _save_option(option, value, f):
 
 def _save_options(section, options, f):
     print_('[%s]' % section, file=f)
-    if isinstance(options, Options):
-        get_option = partial(options.get, last=False)
-    else:
+    try:
+        get_option = partial(options._get, last=False)
+    except AttributeError:
         get_option = options.get
     for option in sorted(options):
         _save_option(option, get_option(option), f)
-- 
2.30.9