Completely split up the templating system from the Matrix Spec template code.

The two are now linked together in build.py by specifying the input module.
Updated gendoc.py to specify the right module.
pull/22/head
Kegan Dougal 9 years ago
parent 8e1d6899c2
commit 5b31c442f5

@ -34,7 +34,12 @@ def rst2html(i, o):
def run_through_template(input): def run_through_template(input):
null = open(os.devnull, 'w') null = open(os.devnull, 'w')
subprocess.check_output( subprocess.check_output(
['python', 'build.py', "-o", "../scripts/tmp", "../scripts/"+input], [
'python', 'build.py',
"-i", "matrix_templates",
"-o", "../scripts/tmp",
"../scripts/"+input
],
stderr=null, stderr=null,
cwd="../templating", cwd="../templating",
) )

@ -1,8 +1,6 @@
This folder contains the templates and templating system for creating the spec. This folder contains the templates and a home-brewed templating system called
We use the templating system Jinja2 in Python. This was chosen over other Batesian for creating the spec. Batesian uses the templating system Jinja2 in
systems such as Handlebars.js and Templetor because we already have a Python Python.
dependency on the spec build system, and Jinja provides a rich set of template
operations beyond basic control flow.
Installation Installation
------------ ------------
@ -12,10 +10,65 @@ Installation
Running Running
------- -------
To build the spec: To pass arbitrary files (not limited to RST) through the templating system:
``` ```
$ python build.py $ python build.py -i matrix_templates /random/file/path/here.rst
``` ```
This will output ``spec.rst`` which can then be fed into the RST->HTML The template output can be found at ``out/here.rst``. For a full list of
converter located in ``matrix-doc/scripts``. options, type ``python build.py --help``.
Developing
----------
## Sections and Units
Batesian is built around the concept of Sections and Units. Sections are strings
which will be inserted into the provided document. Every section has a unique
key name which is the template variable that it represents. Units are arbitrary
python data. They are also represented by unique key names.
## Adding template variables
If you want to add a new template variable e.g. `{{foo_bar}}` which is replaced
with the text `foobar`, you need to add a new Section:
- Open `matrix_templates/sections.py`.
- Add a new function to `MatrixSections` called `render_foo_bar`. The function
name after `render_` determines the template variable name, and the return
value of this function determines what will be inserted.
```python
def render_foo_bar(self):
return "foobar"
```
- Run `build.py`!
## Adding data for template variables
If you want to expose arbitrary data which can be used by `MatrixSections`, you
need to add a new Unit:
- Open `matrix_templates/units.py`.
- Add a new function to `MatrixUnits` called `load_some_data`. Similar to
sections, the function name after `load_` determines the unit name, and the
return value of this function determines the value of the unit.
```python
def load_some_data(self):
return {
"data": "this could be JSON from file from json.loads()",
"processed_data": "this data may have helper keys added",
"types": "it doesn't even need to be a dict. Whatever you want!"
}
```
- You can now call `self.units.get("some_data")` to retrieve the value you
returned.
## Using Jinja templates
Sections can use Jinja templates to return text. Batesian will attempt to load
all templates from `matrix_templates/templates/`. These can be accessed in
Section code via `template = self.env.get_template("name_of_template.tmpl")`. At
this point, the `template` is just a standard `jinja2.Template`. In fact,
`self.env` is just a `jinja2.Environment`.
## Debugging
If you don't know why your template isn't behaving as you'd expect, or you just
want to add some informative logging, use `self.log` in either the Sections
class or Units class. You'll need to add `-v` to `build.py` for these lines to
show.

@ -5,8 +5,10 @@ class AccessKeyStore(object):
"""Storage for arbitrary data. Monitors get calls so we know if they """Storage for arbitrary data. Monitors get calls so we know if they
were used or not.""" were used or not."""
def __init__(self): def __init__(self, existing_data=None):
self.data = {} if not existing_data:
existing_data = {}
self.data = existing_data
self.accessed_set = Set() self.accessed_set = Set()
def keys(self): def keys(self):

@ -0,0 +1,33 @@
"""Parent class for writing sections."""
import inspect
import os
class Sections(object):
"""A class which creates sections for each method starting with "render_".
The key for the section is the text after "render_"
e.g. "render_room_events" has the section key "room_events"
"""
def __init__(self, env, units, debug=False):
self.env = env
self.units = units
self.debug = debug
def log(self, text):
if self.debug:
print text
def get_sections(self):
render_list = inspect.getmembers(self, predicate=inspect.ismethod)
section_dict = {}
for (func_name, func) in render_list:
if not func_name.startswith("render_"):
continue
section_key = func_name[len("render_"):]
section = func()
section_dict[section_key] = section
self.log("Generated section '%s' : %s" % (
section_key, section[:60].replace("\n","")
))
return section_dict

@ -0,0 +1,40 @@
"""Parent class for writing units."""
from . import AccessKeyStore
import inspect
import json
import os
import subprocess
class Units(object):
@staticmethod
def prop(obj, path):
# Helper method to extract nested property values
nested_keys = path.split("/")
val = obj
for key in nested_keys:
val = val.get(key, {})
return val
def __init__(self, debug=False):
self.debug = debug
def log(self, text):
if self.debug:
print text
def get_units(self, debug=False):
unit_list = inspect.getmembers(self, predicate=inspect.ismethod)
unit_dict = {}
for (func_name, func) in unit_list:
if not func_name.startswith("load_"):
continue
unit_key = func_name[len("load_"):]
unit_dict[unit_key] = func()
self.log("Generated unit '%s' : %s" % (
unit_key, json.dumps(unit_dict[unit_key])[:50].replace(
"\n",""
)
))
return unit_dict

@ -39,27 +39,18 @@ Checks
- Any units made which were not used at least once will produce a warning. - Any units made which were not used at least once will produce a warning.
- Any sections made but not used in the skeleton will produce a warning. - Any sections made but not used in the skeleton will produce a warning.
""" """
from batesian import AccessKeyStore
from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template from jinja2 import Environment, FileSystemLoader, StrictUndefined, Template
from argparse import ArgumentParser, FileType from argparse import ArgumentParser, FileType
import importlib
import json import json
import os import os
import sys import sys
from textwrap import TextWrapper from textwrap import TextWrapper
import internal.units
import internal.sections
def load_units():
print "Loading units..."
return internal.units.load()
def load_sections(env, units):
print "\nLoading sections..."
return internal.sections.load(env, units)
def create_from_template(template, sections): def create_from_template(template, sections):
return template.render(sections.data) return template.render(sections)
def check_unaccessed(name, store): def check_unaccessed(name, store):
unaccessed_keys = store.get_unaccessed_set() unaccessed_keys = store.get_unaccessed_set()
@ -67,10 +58,12 @@ def check_unaccessed(name, store):
print "Found %s unused %s keys." % (len(unaccessed_keys), name) print "Found %s unused %s keys." % (len(unaccessed_keys), name)
print unaccessed_keys print unaccessed_keys
def main(file_stream=None, out_dir=None): def main(input_module, file_stream=None, out_dir=None, verbose=False):
if out_dir and not os.path.exists(out_dir): if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir) os.makedirs(out_dir)
in_mod = importlib.import_module(input_module)
# add a template filter to produce pretty pretty JSON # add a template filter to produce pretty pretty JSON
def jsonify(input, indent=None, pre_whitespace=0): def jsonify(input, indent=None, pre_whitespace=0):
code = json.dumps(input, indent=indent, sort_keys=True) code = json.dumps(input, indent=indent, sort_keys=True)
@ -93,7 +86,7 @@ def main(file_stream=None, out_dir=None):
# make Jinja aware of the templates and filters # make Jinja aware of the templates and filters
env = Environment( env = Environment(
loader=FileSystemLoader("templates"), loader=FileSystemLoader(in_mod.exports["templates"]),
undefined=StrictUndefined undefined=StrictUndefined
) )
env.filters["jsonify"] = jsonify env.filters["jsonify"] = jsonify
@ -104,10 +97,12 @@ def main(file_stream=None, out_dir=None):
# load up and parse the lowest single units possible: we don't know or care # load up and parse the lowest single units possible: we don't know or care
# which spec section will use it, we just need it there in memory for when # which spec section will use it, we just need it there in memory for when
# they want it. # they want it.
units = load_units() units = AccessKeyStore(
existing_data=in_mod.exports["units"](debug=verbose).get_units()
)
# use the units to create RST sections # use the units to create RST sections
sections = load_sections(env, units) sections = in_mod.exports["sections"](env, units, debug=verbose).get_sections()
# print out valid section keys if no file supplied # print out valid section keys if no file supplied
if not file_stream: if not file_stream:
@ -132,14 +127,18 @@ def main(file_stream=None, out_dir=None):
if __name__ == '__main__': if __name__ == '__main__':
parser = ArgumentParser( parser = ArgumentParser(
"Process a file (typically .rst) and replace templated areas with spec"+ "Process a file (typically .rst) and replace templated areas with "+
" info. For a list of possible template variables, add"+ "section information from the provided input module. For a list of "+
" --show-template-vars." "possible template variables, add --show-template-vars."
) )
parser.add_argument( parser.add_argument(
"file", nargs="?", type=FileType('r'), "file", nargs="?", type=FileType('r'),
help="The input file to process." help="The input file to process."
) )
parser.add_argument(
"--input", "-i",
help="The python module which contains the sections/units classes."
)
parser.add_argument( parser.add_argument(
"--out-directory", "-o", help="The directory to output the file to."+ "--out-directory", "-o", help="The directory to output the file to."+
" Default: /out", " Default: /out",
@ -150,10 +149,17 @@ if __name__ == '__main__':
help="Show a list of all possible variables you can use in the"+ help="Show a list of all possible variables you can use in the"+
" input file." " input file."
) )
parser.add_argument(
"--verbose", "-v", action="store_true",
help="Turn on verbose mode."
)
args = parser.parse_args() args = parser.parse_args()
if not args.input:
raise Exception("Missing input module")
if (args.show_template_vars): if (args.show_template_vars):
main() main(args.input, verbose=args.verbose)
sys.exit(0) sys.exit(0)
if not args.file: if not args.file:
@ -161,4 +167,7 @@ if __name__ == '__main__':
parser.print_help() parser.print_help()
sys.exit(1) sys.exit(1)
main(file_stream=args.file, out_dir=args.out_directory) main(
args.input, file_stream=args.file, out_dir=args.out_directory,
verbose=args.verbose
)

@ -0,0 +1,9 @@
from sections import MatrixSections
from units import MatrixUnits
import os
exports = {
"units": MatrixUnits,
"sections": MatrixSections,
"templates": os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
}

@ -1,37 +1,11 @@
"""Contains all the sections for the spec.""" """Contains all the sections for the spec."""
from . import AccessKeyStore from batesian import AccessKeyStore
from batesian.sections import Sections
import inspect import inspect
import os import os
class Sections(object): class MatrixSections(Sections):
"""A class which creates sections for each method starting with "render_".
The key for the section is the text after "render_"
e.g. "render_room_events" has the section key "room_events"
"""
def __init__(self, env, units, debug=False):
self.env = env
self.units = units
self.debug = debug
def log(self, text):
if self.debug:
print text
def get_sections(self):
render_list = inspect.getmembers(self, predicate=inspect.ismethod)
section_dict = {}
for (func_name, func) in render_list:
if not func_name.startswith("render_"):
continue
section_key = func_name[len("render_"):]
section = func()
section_dict[section_key] = section
self.log("Generated section '%s' : %s" % (
section_key, section[:60].replace("\n","")
))
return section_dict
def render_room_events(self): def render_room_events(self):
template = self.env.get_template("events.tmpl") template = self.env.get_template("events.tmpl")
@ -64,12 +38,3 @@ class Sections(object):
def render_common_state_event_fields(self): def render_common_state_event_fields(self):
return self._render_ce_type("state_event") return self._render_ce_type("state_event")
def load(env, units):
store = AccessKeyStore()
sections = Sections(env, units)
section_dict = sections.get_sections()
for section_key in section_dict:
store.add(section_key, section_dict[section_key])
return store

@ -1,41 +1,12 @@
"""Contains all the units for the spec.""" """Contains all the units for the spec."""
from . import AccessKeyStore from batesian.units import Units
import inspect import inspect
import json import json
import os import os
import subprocess import subprocess
def prop(obj, path):
# Helper method to extract nested property values
nested_keys = path.split("/")
val = obj
for key in nested_keys:
val = val.get(key, {})
return val
class Units(object): class MatrixUnits(Units):
def __init__(self, debug=False):
self.debug = debug
def log(self, text):
if self.debug:
print text
def get_units(self, debug=False):
unit_list = inspect.getmembers(self, predicate=inspect.ismethod)
unit_dict = {}
for (func_name, func) in unit_list:
if not func_name.startswith("load_"):
continue
unit_key = func_name[len("load_"):]
unit_dict[unit_key] = func()
self.log("Generated unit '%s' : %s" % (
unit_key, json.dumps(unit_dict[unit_key])[:50].replace(
"\n",""
)
))
return unit_dict
def load_common_event_fields(self): def load_common_event_fields(self):
path = "../event-schemas/schema/v1/core" path = "../event-schemas/schema/v1/core"
@ -177,7 +148,9 @@ class Units(object):
) )
# add type # add type
schema["type"] = prop(json_schema, "properties/type/enum")[0] schema["type"] = Units.prop(
json_schema, "properties/type/enum"
)[0]
# add summary and desc # add summary and desc
schema["title"] = json_schema.get("title") schema["title"] = json_schema.get("title")
@ -185,12 +158,14 @@ class Units(object):
# walk the object for field info # walk the object for field info
schema["content_fields"] = get_content_fields( schema["content_fields"] = get_content_fields(
prop(json_schema, "properties/content") Units.prop(json_schema, "properties/content")
) )
# Assign state key info # Assign state key info
if schema["typeof"] == "State Event": if schema["typeof"] == "State Event":
skey_desc = prop(json_schema, "properties/state_key/description") skey_desc = Units.prop(
json_schema, "properties/state_key/description"
)
if not skey_desc: if not skey_desc:
raise Exception("Missing description for state_key") raise Exception("Missing description for state_key")
schema["typeof_info"] = "``state_key``: %s" % skey_desc schema["typeof_info"] = "``state_key``: %s" % skey_desc
@ -245,12 +220,3 @@ class Units(object):
) )
return git_version.encode("ascii") return git_version.encode("ascii")
return "Unknown rev" return "Unknown rev"
def load():
store = AccessKeyStore()
units = Units()
unit_dict = units.get_units()
for unit_key in unit_dict:
store.add(unit_key, unit_dict[unit_key])
return store
Loading…
Cancel
Save