import ast
import re
import string
import yaml
import yamlordereddictloader
from cached_property import cached_property
from .astglobals import find_globals
from .multidict import multidict_items
[docs]class BaseCustomTag(object):
yaml_classes = ()
yaml_tag = None
[docs] @classmethod
def register_in_loader(cls, loader):
yt = cls.yaml_tag
if yt is not None:
loader.add_constructor(yt, cls.from_yaml)
[docs] @classmethod
def register_in_dumper(cls, dumper):
for klass in cls.yaml_classes:
dumper.add_multi_representer(klass, cls.to_yaml)
[docs] @classmethod
def from_yaml(cls, loader, node):
raise NotImplementedError()
[docs] @classmethod
def to_yaml(cls, dumper, data):
raise NotImplementedError()
[docs]class InitKwSetattr(object):
def __init__(self, dict):
for k, v in dict.items(): setattr(self, k, v)
[docs]class CodeTag(InitKwSetattr):
auto_cache = True
env = 'default'
code = None
[docs]class YCodeTag(BaseCustomTag):
yaml_tag = '!c'
[docs] @classmethod
def from_yaml(cls, loader, node):
return CodeTag(dict(code=loader.construct_scalar(node)))
[docs]class YCode2Tag(BaseCustomTag):
yaml_tag = '!c2'
[docs] @classmethod
def from_yaml(cls, loader, node):
return CodeTag(loader.construct_mapping(node))
[docs]class XLoader(yamlordereddictloader.Loader):
pass
for kl in [YCodeTag, YCode2Tag]:
kl.register_in_loader(XLoader)
LINE_START_RE = re.compile('^', re.M)
[docs]class CodeGenerator():
YAMLLoader = XLoader
depth = 0
indent_spaces = 2
[docs] @classmethod
def pyaml_to_python(cls, tree=None, stream=None):
''' use this method! '''
cg = cls()
if stream:
tree = yaml.load(stream, Loader=cg.YAMLLoader)
cg.process(tree) # you probably have a syntax error
return ''.join(cg.result)
@cached_property
def result(self):
return []
[docs] def emit_str(self, text):
self.result.append(text)
[docs] def emit(self, line):
for line_ in line.split('\n'):
self.emit_str(self.get_current_indent())
self.emit_str(line_)
self.emit_str('\n')
[docs] def get_current_indent(self):
return ' '*(self.indent_spaces*self.depth)
[docs] def process(self, yaml_tree):
self.emit_str("""\
# WARNING: This file was autogenerated from a YAML source.
# All modifications to this file WILL be lost.\n\n""")
preamble = yaml_tree.get('preamble', '')
toplevel = __name__.rpartition('.')[0]
preamble = string.Template(preamble).substitute(
pyaml=toplevel,
helper=toplevel+'.helper')
self.python_preamble = preamble
self.emit(preamble)
self.emit_str('\n')
for cls, clsname in yaml_tree.get('classes', {}).items():
self.process_class(cls, clsname)
[docs] def as_isolated_method_in_module(self, preamble, code):
# why is the preamble needed? in case the user makes changes
# to the syntax, e.g. via `from __future__ import braces`, and
# thus the `code` wouldn't parse without said changes
indented_code = LINE_START_RE.sub(' ', code)
modcode = ''.join((preamble, '\ndef f(self):\n', indented_code))
return modcode
[docs] def process_code(self, name, codetag, prefix=''):
self.emit('def {}(self):'.format(name))
self.depth += 1
code = codetag.code.rstrip()
if code.startswith("%"):
code = code[1:]
else:
code = 'return (\n{}\n)'.format(LINE_START_RE.sub(' ', code))
# FIXME: preamble might contain functions and therefore global refs
globals_ = find_globals(ast.parse(
self.as_isolated_method_in_module(self.python_preamble, code)))
globals_ -= {'_', 'AttrPrefixProxy', 'globals', '___globals'}
self.emit('_ = AttrPrefixProxy(self, {!r})\n'
'___globals = globals()'.format(prefix))
for g in globals_:
self.emit("{} = self.PYAML_get_env({!r}, {!r}, ___globals)".format(
g, codetag.env, g))
self.emit(code)
self.depth -= 1
self.emit_str('\n')
self.emit_eval_auto_call(name, codetag)
[docs] def emit_eval_auto_call(self, name, codetag):
# FIXME: cleaner way to do this
before, sep, after = name.partition('EVAL_')
if not before and sep:
self.emit("@property\ndef {}(self):".format(after))
self.depth += 1
self.emit("return self.{}()".format(name))
self.depth -= 1
self.emit_str('\n')
[docs] def process_eval(self, destination_prefix, lookup_prefix, tree):
for origkey, value in multidict_items(tree):
key = lookup_prefix + origkey
if isinstance(value, dict):
self.process_eval(
destination_prefix=destination_prefix,
lookup_prefix=key,
tree=value)
elif isinstance(value, CodeTag):
self.process_code(name=destination_prefix+key, codetag=value,
prefix=lookup_prefix)
else:
raise TypeError("unknown type {!r} for node {!r} key {!r}"
.format(type(value), value, origkey))
[docs] def process_class(self, name, tree):
inherit = tree.get('inherit', ())
self.emit('class {}({}):'.format(name, ', '.join(inherit)))
self.depth += 1
for key, value in multidict_items(tree):
if key == 'eval':
self.process_eval(
destination_prefix='EVAL_', lookup_prefix='', tree=value)
elif key == 'env':
self.process_eval(
destination_prefix='ENV_', lookup_prefix='', tree=value)
self.depth -= 1
self.emit_str('\n')
# s = open('simudo/test/pyaml1_test1.yaml', 'rt')
# print(CodeGenerator.pyaml_to_python(stream=s))