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