Commit 94182f40 authored by Vincent Pelletier's avatar Vincent Pelletier Committed by Xavier Thompson

[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.
parent 8f0ecb75
...@@ -56,6 +56,7 @@ import shutil ...@@ -56,6 +56,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
from functools import partial from functools import partial
...@@ -94,6 +95,66 @@ def print_(*args, **kw): ...@@ -94,6 +95,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
...@@ -1602,7 +1663,17 @@ class Options(DictMixin): ...@@ -1602,7 +1663,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('$${', '${')
...@@ -1687,7 +1758,7 @@ class Options(DictMixin): ...@@ -1687,7 +1758,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
...@@ -1700,14 +1771,14 @@ class Options(DictMixin): ...@@ -1700,14 +1771,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):
...@@ -1736,6 +1807,9 @@ class Options(DictMixin): ...@@ -1736,6 +1807,9 @@ class Options(DictMixin):
result = copy.deepcopy(self._raw) result = copy.deepcopy(self._raw)
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):
...@@ -1808,6 +1882,8 @@ def _quote_spacey_nl(match): ...@@ -1808,6 +1882,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:]
...@@ -1817,9 +1893,9 @@ def _save_option(option, value, f): ...@@ -1817,9 +1893,9 @@ 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)
if isinstance(options, Options): try:
get_option = partial(options.get, last=False) get_option = partial(options._get, last=False)
else: except AttributeError:
get_option = options.get get_option = options.get
for option in sorted(options): for option in sorted(options):
_save_option(option, get_option(option), f) _save_option(option, get_option(option), f)
......
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