Commit c16c9051 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 6721aa25
......@@ -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)
......
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