Source code for zoti_gen.core

import sys
from collections import OrderedDict
from dataclasses import dataclass, field
from pprint import pformat
from typing import Dict, List, Optional

import marshmallow as mm
import networkx as nx
import zoti_yaml as zoml
from zoti_gen.jinja_extensions import __zoti_gen_env__
from zoti_gen.exceptions import TemplateError

ATTR_NAME = "name"
ATTR_BLOCK = "block"
ATTR_TYPE = "type"
ATTR_PH = "placeholder"
ATTR_CODE = "code"
ATTR_PROTO = "prototype"
ATTR_USAGE = "usage"

PRAGMA_PASS = "pass"
PRAGMA_NEW = "new"
PRAGMA_EXP = "expand"

FUN_CHECK = "check"          # member of library component
FUN_LTOL = "label_to_label"  # member of ProjHandler.resolve._map_bindings
FUN_UTOL = "usage_to_label"  # member of ProjHandler.resolve._map_bindings
FUN_PTOP = "param_to_param"  # member of ProjHandler.resolve._map_bindings
FUN_VTOP = "value_to_param"  # member of ProjHandler.resolve._map_bindings
FUN_PTOL = "param_to_label"  # member of ProjHandler.resolve._map_bindings

KEYS_PRAGMA = [PRAGMA_PASS, PRAGMA_NEW, PRAGMA_EXP]
KEYS_BIND = [FUN_LTOL, FUN_UTOL, FUN_PTOP, FUN_VTOP, FUN_PTOL]


class Nested(mm.fields.Nested):
    def __init__(self, nested, **kwargs):
        super(Nested, self).__init__(nested, **kwargs)

    def _deserialize(self, node, attr, data, **kwargs):
        try:
            return super(Nested, self)._deserialize(node, attr, data, **kwargs)
        except mm.ValidationError as error:
            if zoml.get_pos(node):
                error.messages = [str(zoml.get_pos(node)), error.messages]
            raise error


[docs]class Template: """Container for a Jinja template.""" string: str """formatted string template""" _parent: str """name of the parent node of this template function (for debugging)""" def __init__(self, string, parent=None): self._parent = parent self.string = string def __repr__(self): return self.string def __bool__(self): return bool(self.string) def render(self, label={}, param={}, placeholder={}, info=None, **kwargs) -> str: context = { "label": {k: LabelSchema().dump(p) for k, p in label.items()}, "param": param, "placeholder": placeholder, } context.update(kwargs) try: tm = __zoti_gen_env__.from_string(self.string) return tm.render(**context) except Exception: ty, msg, exc_tb = sys.exc_info() while exc_tb and "template code" not in exc_tb.tb_frame.f_code.co_name: exc_tb = exc_tb.tb_next lineno = exc_tb.tb_lineno if exc_tb else -2 raise TemplateError(self.string, context, err_line=lineno, err_string=repr(msg), info=info, parent=self._parent)
class TemplateField(mm.fields.Field): """A template function is a Shell-like formatted string where all the variables are exposed as arguments. This function is meant to be called by the `Rendering <rendering>`_ engine which would fill in the arguments. The formatted string syntax is documented `here <https://docs.python.org/3/library/string.html#template-strings>`_. """ def _deserialize(self, node, attr, data, **kwargs): if not isinstance(node, str): raise mm.ValidationError("Expected string template.") return Template(node, parent=attr) def _serialize(self, obj, attr, data, **kwargs): if isinstance(obj, str): return obj else: return obj.string ############### # REQUIREMENT # ###############
[docs]class Requirement: """ Illustrates prerquisites. Stores input iterables (e.g., lists) as dependency graphs. :param requirement: dictionary of iterables. """ requirement: Dict[str, nx.DiGraph] """ Dictionary of dependency graphs. """ def __init__(self, requirement, **kwargs): self.requirement = { key: nx.path_graph(req) for key, req in requirement.items()} def __repr__(self) -> str: ret = pformat({ key: list(graph.nodes()) for key, graph in self.requirement.items() }) return f"Requirement({ret})"
[docs] def update(self, other: Optional["Requirement"]): """merges two ``Requirement`` entries updating the dependency graphs.""" if other: for key, graph in other.requirement.items(): if key in self.requirement: self.requirement[key].update(graph) else: self.requirement[key] = graph
[docs] def dep_list(self, key) -> List: """Returns a list with the solved dependencies for a certain requirement type.""" return list(nx.dfs_preorder_nodes(self.requirement[key]))
def as_dict(self) -> Dict: return {key: self.dep_list(key) for key in self.requirement.keys()}
# class RequirementSchema(mm.Schema): # """Illustrates prerequisites for the parent element. Internally it is # represented using a :class:`zoti_gen.core.Requirement` class. It may # contain the following entries: # :include: (list) files or modules to be included in the preamble # of the generated target artifact # """ # include = mm.fields.List(mm.fields.Str()) # @mm.post_load # def make(self, data, **kwargs): # return Requirement(requirement=data) class RequirementField(mm.fields.Field): """Illustrates prerequisites for the parent element. Internally it is represented using a :class:`zoti_gen.core.Requirement` class. It may contain the following entries: """ def _deserialize(self, node, attr, data, **kwargs): if not isinstance(node, dict): raise mm.ValidationError("Expected dict requirement.") return Requirement(requirement=node) def _serialize(self, obj, attr, data, **kwargs): return obj.requirement if obj is not None else None ############ # INSTANCE # ############
[docs]@dataclass(repr=False) class Ref: """Hashable reference to a user block or a library component.""" module: str """qualified name of module""" name: str """name of block""" def __repr__(self): return f"{self.module}.{self.name}" def __hash__(self): return hash("{self.module}{self.name}")
class RefSchema(mm.Schema): """A reference is an entry pointing to an object by its qualified name and/or path. Since ZOTI-Gen documents are flat (i.e., they consist in a flat list of block descriptions), and the only objects referenced in ZOTI-Gen are blocks, the only access mechanism implemented is referencing by (block) name. Hence, every reference entry will have the following mandatory fields: :module: *(string)* the full (dot-separated) name of module containing the referenced block, even if that means the current module. :name: *(string)* the name of the referenced block. For less verbose reference syntax one could check the ``!ref`` keyword in the `ZOTI-YAML <../zoti-yaml>`_ language extension and pre-processor. """ module = mm.fields.String(required=True) name = mm.fields.String(required=True) @mm.post_load def make_ref(self, data, **kwargs): return Ref(**data)
[docs]@dataclass class Bind: """Deserialized version of a binding, containing directily bind resolver arguments.""" func: str """name of the binding function (see schema entries)""" args: Dict """arguments passed to the binding function""" _info: Dict = field(default_factory=lambda: {})
class BindSchema(mm.Schema): """A binding between one of the labels or parameters of the parent block and a label or parameter of the referenced block. Internally it is represented using a :class:`zoti_gen.core.Bind` class. It needs to contain *only one* of the following entries: :label_to_label: (dict) :parent: (str) ID of parent :child: (str) ID of child :usage: (`Template <#code-template>`_) rendered with context: .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: bind/label_to_label :end-before: # CONTEXT-END: bind/label_to_label :usage_to_label: (dict) :child: (str) ID of child :usage: (`Template <#code-template>`_) rendered with context: .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: bind/usage_to_label :end-before: # CONTEXT-END: bind/usage_to_label :param_to_param: (dict) :parent: (str) Parent parameter :child: (str) Child parameter :value_to_param: (dict) :value: (str) Value as simple string :child: (str) Child parameter """ label_to_label = mm.fields.Nested( mm.Schema.from_dict( { "parent": mm.fields.String(required=True), "child": mm.fields.String(required=True), "usage": TemplateField(required=False), } ) ) usage_to_label = mm.fields.Nested( mm.Schema.from_dict( { "child": mm.fields.String(required=True), "usage": TemplateField(required=False), } ) ) param_to_param = mm.fields.Nested( mm.Schema.from_dict( { "parent": mm.fields.String(required=True), "child": mm.fields.String(required=True), } ) ) value_to_param = mm.fields.Nested( mm.Schema.from_dict( { "child": mm.fields.String(required=True), "value": mm.fields.String(required=True), } ) ) param_to_label = mm.fields.Nested( mm.Schema.from_dict( { "parent": mm.fields.String(required=True), "child": mm.fields.String(required=True), } ) ) _info = mm.fields.Raw(data_key="_info", load_default={}) @mm.post_load def make(self, data, **kwargs): func, args = [(k, v) for k, v in data.items() if k in KEYS_BIND][0] return Bind(func, args, data["_info"]) @mm.post_dump(pass_original=True) def dump_bind(self, obj, orig, **kwargs): return {orig.func: orig.args} @mm.validates_schema def validate_bind(self, data, **kwargs): allowed = [k for k in data.keys() if k in KEYS_BIND] if len(allowed) != 1: msg = f"Only one of the following key is allowed: {repr(KEYS_BIND)}" raise mm.ValidationError(msg)
[docs]@dataclass # (init=False) class Instance: """Entry binding a placeholder in the parent's template code to another block. """ placeholder: Optional[str] """ ID for the template placeholder """ block: Optional[Dict] """ ID of the block referenced to fill the placeholder """ directive: List[str] bind: List[Bind] """ list of bindings between the parent block and referenced block """ usage: Template """ Target-dependent template passed by type system """ _info: Dict = field(default_factory=lambda: {})
class InstanceSchema(mm.Schema): """Refers to another block and (at minimum) triggers its evaluation by the the `Rendering <rendering>`_ engine. It can define an inclusion relation between the parent and the referenced blocks, in which case the referenced one would occupy the space pointed out by a *placeholder* markup in the parent's template. Furthermore, the relation between the two blocks can be enforced by a set of *bindings* that connects the labels and parameters of the two blocks. Internally, aln instance is represented using a :class:`zoti_gen.core.Instance` class. To define a block instance within the parent block the following entries might be used: :block: (`Reference <#reference>`_) points to an existing (i.e. loaded) block. :placeholder: (string) the name of the placeholder, as it appears in the parent's `Code Template <#code-template>`_. :directive: (list of strings) directive flags passed to the `Rendering <rendering>`_ engine. :`bind <#bind>`_: (list) bindings between the labels and parameters of the parent block and those of the referenced block. :usage: (`Template <#code-template>`_) defines how this block is being instantiated in case it is not expanded inline (e.g., as function call). The template string is defined by the type system. It is rendered with the following contexts, depending on which directive is passed: - ``expand`` is in directives .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: instance/usage-expand :end-before: # CONTEXT-END: instance/usage-expand - ``expand`` is not in directives .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: instance/usage-noexpand :end-before: # CONTEXT-END: instance/usage-noexpand """ placeholder = mm.fields.String(required=True, allow_none=True) block = mm.fields.Nested(RefSchema, required=True, allow_none=True) directive = mm.fields.List( mm.fields.String(validate=mm.validate.OneOf(KEYS_PRAGMA)), required=False, load_default=[] ) bind = mm.fields.List(Nested(BindSchema), required=False, load_default=[]) # usage = mm.fields.String(required=False) usage = TemplateField(required=False, allow_none=True, load_default=Template("", parent="instance/usage")) _info = mm.fields.Raw(data_key="_info", load_default={}) @mm.post_load def make(self, data, **kwargs): return Instance(**data) ######### # LABEL # #########
[docs]@dataclass class Label: """ Carries information about labels (filled in by type system) """ name: str """ Unique name in the scope of a block""" usage: Template """ Default usage template. Called on top-level (non-binding) instances.""" glue: Dict """Dictionary of templates passed from the type system.""" _info: Dict = field(default_factory=lambda: {})
class LabelSchema(mm.Schema): """A label is the low-level code equivalent of a 'port'. Its function is to provide a name which can be used in bindings and glue generation. Internally it is represented using a :class:`zoti_gen.core.Label` class. :name: (string) unique ID in the scope of the parent block :glue: (dict) entries with glue code tailored for various circumstances, provided by the type system and accessible from within the code template using the `label.<port_id>.glue` key. :usage: (`Template <#code-template>`_) defines how this label is to be expanded in the code. Provided by the type system. Rendered with the following context: .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: label/usage :end-before: # CONTEXT-END: label/usage """ name = mm.fields.Str(required=True) usage = TemplateField(required=True) glue = mm.fields.Mapping( keys=mm.fields.String( required=True, # , validate=mm.validate.NoneOf(["name"]) ), # values=TemplateField(), # TODO values=mm.fields.Raw(), required=False, allow_none=True, load_default={}) _info = mm.fields.Raw(data_key="_info", load_default={}) @mm.post_load def make(self, data, **kwargs): return Label(**data) class LabelListField(mm.fields.List): def __init__(self, **kwargs): self.base = Nested(LabelSchema) super(LabelListField, self).__init__(self.base, **kwargs) def _serialize(self, pdict, attr, obj, **kwargs): if pdict is None: return [] return [ self.base._serialize(p, attr, dict(pdict), **kwargs) for p in pdict.values() ] def _deserialize(self, plist, attr, data, **kwargs): if plist is None: return None names = [p["name"] for p in plist] if len(names) != len(set(names)): raise mm.ValidationError("Duplicate names in label list") return OrderedDict([ (p["name"], self.base._deserialize(p, attr, plist, **kwargs)) for p in plist ]) ######### # BLOCK # #########
[docs]@dataclass class Block: """ Base class for block structure. """ class Schema(mm.Schema): """A block describes a unit of code that might be related to other blocks through bindings and might contain a template. Internally it is represented using a :class:`zoti_gen.core.Block` class. Blocks are described using the following entries; :name: (mandatory, string) the unique ID of the block :type: (`Reference <#reference>`_) points to an externally-defined library template which would fill in the corresponding entries below, as documented in `Template Libraries <template-libs>`_ page. :`requirement <#requirement>`_: (dict) block prerequisites :`label <#label>`_: (list) label entries, each with a unique name :param: (dict) generic parameters passed as-is to the template renderer, accessed with the `param` key. :`instance <#instance>`_: (list) other blocks instantiated from this one. :code: (`Template <#code-template>`_) containing this block's `Code Template <#code-template>`_. Rendered witn the context: .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: prototype :end-before: # CONTEXT-END: prototype :prototype: (`Template <#code-template>`_) defines this block's type signature, as provided from a type system. Rendered with the following context: .. literalinclude:: ../../src/zoti_gen/builder.py :language: python :start-after: # CONTEXT-BEGIN: code :end-before: # CONTEXT-END: code """ name = mm.fields.String(required=True) requirement = RequirementField() label = LabelListField() param = mm.fields.Mapping(required=False) code = TemplateField(allow_none=True) prototype = TemplateField(allow_none=True) instance = mm.fields.List(Nested(InstanceSchema), required=False) _info = mm.fields.Raw(data_key="_info", load_default={}) @mm.post_load def make(self, data, **kwargs): return Block(**data) name: str """ Unique ID of block. """ prototype: Template = field(default=Template("", parent="prototype")) """ Target dependent function signature provided by the type system """ requirement: Optional[Requirement] = None """ Block prerequisites.""" label: OrderedDict[str, Label] = field( default_factory=lambda: OrderedDict()) """ Ordered dictionary of labels """ param: Dict = field(default_factory=lambda: {}) """ Generic parameters """ code: Optional[str] = None """ Target code for block, either as Jinja template or as raw text """ instance: List[Instance] = field(default_factory=lambda: []) """list of instances that bind template (code) placeholders to other blocks""" _info: Dict = field(default_factory=lambda: {}) @property def getLabelKeys(self): return list(self.label.keys()) @property def getLabelNames(self): return [l.name for l in self.label.values()] @property def getInstancePlaceholders(self): return [i.placeholder for i in self.instance] @property def getInstanceBlocks(self): return [repr(i.block) for i in self.instance] @property def getType(self): return type(self)