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