Commit 4e13dcb9 authored by Vincent Pelletier's avatar Vincent Pelletier Committed by Julien Muchembled

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.
parent 6a2e40d8
...@@ -47,6 +47,7 @@ import shutil ...@@ -47,6 +47,7 @@ import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
import pprint
import zc.buildout import zc.buildout
import zc.buildout.download import zc.buildout.download
...@@ -65,6 +66,66 @@ def print_(*args, **kw): ...@@ -65,6 +66,66 @@ def print_(*args, **kw):
file = sys.stdout file = sys.stdout
file.write(sep.join(map(str, args))+end) 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 realpath = zc.buildout.easy_install.realpath
_isurl = re.compile('([a-zA-Z0-9+.-]+)://').match _isurl = re.compile('([a-zA-Z0-9+.-]+)://').match
...@@ -1387,7 +1448,17 @@ class Options(DictMixin): ...@@ -1387,7 +1448,17 @@ class Options(DictMixin):
for s in v.split('$$')]) for s in v.split('$$')])
self._cooked[option] = v 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: try:
if last: if last:
return self._data[option].replace('$${', '${') return self._data[option].replace('$${', '${')
...@@ -1462,7 +1533,7 @@ class Options(DictMixin): ...@@ -1462,7 +1533,7 @@ class Options(DictMixin):
options = self.buildout[section] options = self.buildout[section]
finally: finally:
del self.buildout._initializing[-1] 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 v is None:
if option == '_buildout_section_name_': if option == '_buildout_section_name_':
v = self.name v = self.name
...@@ -1475,14 +1546,14 @@ class Options(DictMixin): ...@@ -1475,14 +1546,14 @@ class Options(DictMixin):
return ''.join([''.join(v) for v in zip(value[::2], subs)]) return ''.join([''.join(v) for v in zip(value[::2], subs)])
def __getitem__(self, key): def __getitem__(self, key):
v = self.get(key) v = self.get(key, _MARKER)
if v is None: if v is _MARKER:
raise MissingOption("Missing option: %s:%s" % (self.name, key)) raise MissingOption("Missing option: %s:%s" % (self.name, key))
return v return v
def __setitem__(self, option, value): def __setitem__(self, option, value):
if not isinstance(value, str): if not isinstance(value, str):
raise TypeError('Option values must be strings', value) value = dumps(value)
self._data[option] = value self._data[option] = value
def __delitem__(self, key): def __delitem__(self, key):
...@@ -1511,6 +1582,9 @@ class Options(DictMixin): ...@@ -1511,6 +1582,9 @@ class Options(DictMixin):
result = self._raw.copy() result = self._raw.copy()
result.update(self._cooked) result.update(self._cooked)
result.update(self._data) result.update(self._data)
for key, value in result.items():
if value.startswith(SERIALISED_VALUE_MAGIC):
result[key] = loads(value)
return result return result
def _call(self, f): def _call(self, f):
...@@ -1583,6 +1657,8 @@ def _quote_spacey_nl(match): ...@@ -1583,6 +1657,8 @@ def _quote_spacey_nl(match):
return result return result
def _save_option(option, value, f): def _save_option(option, value, f):
if not isinstance(value, str):
value = dumps(value)
value = _spacey_nl.sub(_quote_spacey_nl, value) value = _spacey_nl.sub(_quote_spacey_nl, value)
if value.startswith('\n\t'): if value.startswith('\n\t'):
value = '%(__buildout_space_n__)s' + value[2:] value = '%(__buildout_space_n__)s' + value[2:]
...@@ -1592,10 +1668,12 @@ def _save_option(option, value, f): ...@@ -1592,10 +1668,12 @@ def _save_option(option, value, f):
def _save_options(section, options, f): def _save_options(section, options, f):
print_('[%s]' % section, file=f) print_('[%s]' % section, file=f)
items = list(options.items()) try:
items.sort() get_option = options._get
for option, value in items: except AttributeError:
_save_option(option, value, f) get_option = options.get
for option in sorted(options):
_save_option(option, get_option(option), f)
def _default_globals(): def _default_globals():
"""Return a mapping of default and precomputed expressions. """Return a mapping of default and precomputed expressions.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment