Template Libraries

Template libraries are a key part of ZOTI-Gen and enable the collaborative development, organization and usage of templates from different sources and for different target platforms.

Template Modules

Instead of providing an own library management system, ZOTI-Gen chooses to “piggyback” on the already-established module and package system of Python. Hence ZOTI-Gen template libraries are developed as regular Python packages, taking advantage of the following features:

  • the ability to organize components containing templates into modules and sub-modules according to a certain design classification;

  • both the API documentation tool ecosystem for Python projects and the CI/CD features that come with developing a software project.

  • the ability to define different packages of components under various MIT-compatible licenses (both open-source and proprietary) and use them together in the same code generation flow;

  • the ability to enhance template components (whose principal purpose is to contain specification data) with custom program logic used for various purposes, e.g., to verify the sanity of the importer.

In short, ZOTI-Gen template libraries are Python packages exporting user-defined template components. Each template component is a child class inheriting the zoti_gen.core.Block base class and partially defining its members.

Using Template Components

A template component can be instantiated from the ZOTI-Gen input specification through the type entry of blocks. For example, consider the Baz component defined in module Foo.Bar:

# module Foo.Bar

from dataclasses import dataclass
from zoti_gen import Block, Requirement

@dataclass
class Baz(Block):
	code: str = "Hello {{ param.world }}!"
    requirement: Requirement = field(
        default=Requirement({"include": ["<stdio.h>"]})
    )

Provided that Foo.Bar is accessible from the ZOTI-Gen tool (e.g., by including its root to PYTHONPATH), then the specification

block:
- name: baz
  type: {module: "Foo.Bar", name: "Baz"}
  param: {world: World}

is equivalent to

block:
- name: baz
  param: {world: World}
  code: |
    Hello {{ param.world }}!
  requirement: 
    include: ["<stdio.h>"]

Developing Template Components

NOTE: All utilities in this section are re-exported by zoti_gen.

Auto-Generated (Mandatory) Schema

Since each component is a different class, we need to use separate schemas for input deserialization and validation. Luckily ZOTI-Gen provides a class wrapper for inheriting a base schema with default definition;

zoti_gen.util.with_schema(schema_base_class)[source]

Wrapper which creates a sub-class Schema under the current class, as a (default) child of schema_base_class

Example usage:

@with_schema(Block.Schema)
@dataclass
class ReadArray(Block):
    pass

will create a class ReadArray.Schema which is used whenever instantiating this class with data from the input specification file.

Alternatively, if a custom validation schema is required, it should be defined accordingly.

Including code from external sources

The main reason to have template libraries is not to have to deal with target-specific code during the synthesis process, but rather to “pick-and-place” from a set of pre-written (and ideally pre-validated) components. As such, most of the time the main reason to define a template component would be for its code member. Since Python strings are not ideal for developing in target-specific syntax, ZOTI-Gen provides the following utility:

zoti_gen.util.read_at(module: str, filename: str, name: str, delimiters: Tuple[str, str] = ('// *Template: *{name}', '// *End: *{name}')) str[source]

Returns a template string from a (possibly formatted) external source file. A source file may contain multiple templates, hence this method returns only the one between delimiter for name.

Parameters:
  • module – name of Python module relative to which the source file is found (e.g. can use __name__ if it is in the same path as the current file)

  • filename – full name of the source file, including extension.

  • name – name of the current component, used as delimiter marker in case multiple components are defined in the same file.

  • delimiters – tuple containing the begin and end marker for the issued template, formatted as regular expressions where {name} is replaced with name.

Validator hook

While schema validation is important, it does not suffice when the the current component has some specific dependencies based on its context and relation with other blocks. Hence the developer can define a member function check which is called by the renderer after resolving the entire code blocks structure, for example:

def check(self):
	assert "format" in self.param
	assert "arg" in self.label

Documentation

Do not forget to use docstrings and an API documentation tool of your choice to document your template libraries. A library is not of much use if no one can use it. Make sure you mark clearly and visibly what is the purpose, content and requirements for each component in order to facilitate its finding and proper usage in synthesis flows.