From 01e63a63656c1cc4db38836c77f89f6cc32aae5a Mon Sep 17 00:00:00 2001
From: Vincent Pelletier <vincent@nexedi.com>
Date: Wed, 18 Apr 2012 09:54:36 +0200
Subject: [PATCH] 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 | 65 ++++++++++++++++++++++++++++++++++---
 1 file changed, 60 insertions(+), 5 deletions(-)

diff --git a/src/zc/buildout/buildout.py b/src/zc/buildout/buildout.py
index c733a263..d27ba0cd 100644
--- a/src/zc/buildout/buildout.py
+++ b/src/zc/buildout/buildout.py
@@ -36,10 +36,65 @@ import tempfile
 import UserDict
 import warnings
 import subprocess
+import pprint
 import zc.buildout
 import zc.buildout.download
 import zc.buildout.easy_install
 
+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,
+    }}
+
+    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
 
@@ -1289,11 +1344,13 @@ class Options(UserDict.DictMixin):
         v = self.get(key)
         if v is None:
             raise MissingOption("Missing option: %s:%s" % (self.name, key))
+        elif v.startswith(SERIALISED_VALUE_MAGIC):
+            v = loads(v)
         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):
@@ -1412,10 +1469,8 @@ def _save_option(option, value, f):
 
 def _save_options(section, options, f):
     print >>f, '[%s]' % section
-    items = options.items()
-    items.sort()
-    for option, value in items:
-        _save_option(option, value, f)
+    for option in sorted(options.keys()):
+        _save_option(option, options.get(option), f)
 
 def _open(base, filename, seen, dl_options, override, downloaded):
     """Open a configuration file and return the result as a dictionary,
-- 
2.30.9