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.
This commit is contained in:
parent
8e1d6899c2
commit
5b31c442f5
11 changed files with 196 additions and 114 deletions
|
@ -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):
|
33
templating/batesian/sections.py
Normal file
33
templating/batesian/sections.py
Normal file
|
@ -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
|
40
templating/batesian/units.py
Normal file
40
templating/batesian/units.py
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
|
9
templating/matrix_templates/__init__.py
Normal file
9
templating/matrix_templates/__init__.py
Normal file
|
@ -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…
Add table
Add a link
Reference in a new issue