Source code for sondra.help

from docutils.core import publish_string, publish_parts
import io
import logging
import json
import jsonschema


[docs]class RSTBuilder(object): """A simple builder for reStructuredText help. Does not support all of RST by a long shot. However, it provides sufficient functionality to be useful for generating help from JSON. """ _log = logging.getLogger('RSTBuilder') HEADING_CHARS = """#=*+-~^`_:.'""" def __init__(self, fmt='rst', out=None, initial_heading_level=0): self._out = out or io.StringIO() self._cached_html = None self._cached_text = None self._cached_odf = None self._output = fmt self._indents = [] self._list = [] # a list of "*" or number. Each is a list level. self._heading_level = initial_heading_level self._indent_str = '' self.indent_amt = 2 self._lines_written = 0 def __str__(self): if self._output == 'rst': return self.rst else: return self.html
[docs] def build(self): self._log.debug("Build begun")
@property
[docs] def odt(self): if not self._cached_odf: self._log.debug("Creating ODT") self._cached_odf = publish_string(self.rst, writer_name='odf_odt').decode('utf-8') return self._cached_odf
@property
[docs] def html(self): if not self._cached_html: self._log.debug("Creating HTML") self._cached_html = publish_parts(self.rst, writer_name='html')['html_body'] return self._cached_html
@property
[docs] def rst(self): self._cached_text = self._out.getvalue() if not self._cached_text: self._log.debug("Creating reStructuredText") self.build() self._log.debug("Finished creating reStructuredText.") self._out.flush() self._cached_text = self._out.getvalue() return self._cached_text
[docs] def indent(self, amt=None): amt = amt or self.indent_amt self._indents.append(amt) ct = sum(self._indents) self._indent_str = ' ' * ct
[docs] def dedent(self): self._indents.pop() ct = sum(self._indents) self._indent_str = ' ' * ct
[docs] def full_stop(self): self._list = [] self._heading_level = 0 self._indents = [] self._indent_str = ''
[docs] def sep(self, separator=' '): self._out.write(separator)
[docs] def begin_subheading(self, title): self.end_lists() self.line(title) self.line(self.HEADING_CHARS[self._heading_level] * len(title)) self.line() self._heading_level += 1
[docs] def end_subheading(self): self._heading_level -= 1 if self._heading_level < 0: self._heading_level = 0 if self._lines_written: self.line() if self._heading_level == 0: self.line() self._lines_written = 0
[docs] def begin_code(self, preface=''): self.end_lists() self.line(preface+'::') self.line() self.indent(4)
[docs] def end_code(self): self._indents = [] self.line()
[docs] def begin(self, title): if len(self._list) > 0: self.line(title) self.begin_list() else: self.begin_subheading(title)
[docs] def end(self): if len(self._list) > 0: self.end_list() else: self.end_subheading()
[docs] def end_lists(self): self._list = [] self._indents = [] # def _begin_line(self): # if len(self._list): # self._out.write(self._indent_str) # self._out.write(self._list[-1]) # self._out.write(" ") # self._indent(2) # else: # self._out.write(self._indent_str) # # def _write(self, s): # self._out.write(s) # # def _end_line(self): # self._out.write('\n')
[docs] def line(self, s=''): s = [l.strip() for l in s.splitlines()] first, *rest = s if s else (None, []) if first: if len(self._list): self._out.write(self._indent_str) self._out.write(self._list[-1]) self._out.write(" ") self.indent(2) self._out.write(first) self._out.write('\n') else: self._out.write(self._indent_str) self._out.write(first) self._out.write('\n') for l in rest: self._out.write(self._indent_str) self._out.write(l) self._out.write('\n') if self._list: self.dedent() else: self._out.write('\n') self._lines_written += 1
[docs] def define(self, term, definition): self.line("**{term}** - {definition}".format(term=term, definition=definition))
[docs] def begin_list(self): if len(self._list): self.indent() self.line() char = len(self._list) % 3 markers = "*-+" self._list.append(markers[char])
[docs] def end_list(self): self._list.pop() if len(self._list): self.dedent() elif self._lines_written: self.line() self._lines_written = 0
[docs]class SchemaHelpBuilder(RSTBuilder): """Builds help from a JSON-Schema as either HTML or reStructuredText source.""" RESERVED_WORDS = { "type", "title", "description", "name", "id", "properties", "patternProperties", "additionalProperties", "maxProperties", "minProperties", "required", "dependencies", } _log = logging.getLogger("SchemaHelpBuilder") def __init__(self, schema, url="", fmt='rst', out=None, initial_heading_level=0): """Builds help from a JSON-Schema as either HTML or reStructuredText source. Args: schema: a json-schema as a python dict. output (str): either `"html"` or `"rst"` """ super(SchemaHelpBuilder, self).__init__(fmt=fmt, out=out, initial_heading_level=initial_heading_level) # validate a schema before we write help for it. jsonschema.Draft4Validator.check_schema(schema) self.schema = schema self.url = url or schema.get('id', url) self._refs = {} def _make_definition(self, name, subschema): if "id" in subschema: ref_name = "#" + subschema['id'] else: ref_name = "#/definitions/{name}".format(name=name) self._refs[ref_name] = subschema return ref_name def _gather_definitions(self): for name, subschema in self.schema.get('definitions', {}).items(): ref_name = self._make_definition(name, subschema) self._log.debug("Found {0} in definitions, caching as {1}.".format(name, ref_name))
[docs] def build(self): super(SchemaHelpBuilder, self).build() self._gather_definitions() self._write_fragment(self.schema) if "definitions" in self.schema and self.schema['definitions']: self.begin_subheading("Definitions") for name, ref in self.schema["definitions"].items(): self._write_fragment(ref, name=name) self.end_subheading() self._log.debug("Build finished.")
def _write_link_or_fragment(self, frag, name=None, title=None): if "$ref" in frag: self.line('`{link}`_'.format(link=self._link_name(frag["$ref"]))) else: self._write_fragment(frag, name=name, title=title) def _write_fragment(self, frag, name=None, title=None): fragment_type = self._type_specifier(frag) assigned_title = title is not None if not title: # set the title of the fragment if name: title = "{name}".format(name=name) elif 'title' in frag: title = frag['title'] if title: self.begin(title) if 'name' in frag: self.define('Name', frag['name']) self.line() if 'id' in frag: self.define('Id', "``" + frag['id'] + "``") self.line() if not assigned_title: if "type" in frag: if 'refersTo' in frag: self.define("Refers To", frag['refersTo']) else: self.define('Type', frag['type']) self.line() if "description" in frag: self.define("Description", frag['description']) if "default" in frag: self.define("Default", frag['default']) self.line() self._validation_specifiers(frag) if fragment_type == 'object': self._write_object(frag) elif fragment_type == 'array': self._write_array(frag) elif fragment_type == 'string': self._write_string(frag) elif fragment_type == 'integer': self._write_numeric(frag) elif fragment_type == 'float': self._write_numeric(frag) else: self._write_object(frag) if title: self.end() def _write_property(self, name, typedef): title = "**{name}** {type_specifier}{default} - {description}".format( name=name, default=(" = ``{0}``".format(typedef['default'])) if 'default' in typedef else '', type_specifier=self._type_specifier(typedef), description=self._description(typedef) ) self._write_fragment(typedef, name=name, title=title) def _write_property_list(self, props): self.begin_list() for prop, defn in sorted(props.items(), key=lambda x: x[0]): self._write_property(prop, defn) self.end_list() def _write_object(self, typedef): self.begin_list() if "minProperties" in typedef: self.define("Min Properties", typedef['minProperties']) if "maxProperties" in typedef: self.define("Max Properties", typedef['maxProperties']) if "required" in typedef: self.define("Required", ", ".join(typedef['required'])) self.end_list() if "dependencies" in typedef: self.begin('Dependencies') for name, deps in typedef['dependencies'].items(): deps = typedef['dependencies'] if isinstance(deps, list): self.define("Property dependency on **{0}**".format(name), ', '.join(deps)) else: self.begin("Schema dependency on {0}".format(name)) self._write_fragment(deps) self.end() self.end() if "properties" in typedef: self.begin("Properties") self._write_property_list(typedef['properties']) self.end() if "additionalProperties" in typedef: if isinstance(typedef['additionalProperties'], bool): self.define("Additional properties", typedef['additionalProperties']) else: self.begin("Additional Properties:") self._write_property_list(typedef['additionalProperties']) self.end() if "patternProperties" in typedef: self.begin("Pattern Properties") self._write_property_list(typedef['patternProperties']) self.end() def _write_array(self, typedef): if "items" in typedef: if isinstance(typedef['items'], list): self.begin_list() for x in typedef['items']: self._write_link_or_fragment(x) self.end_list() else: self.begin("**Items**") self._write_link_or_fragment(typedef['items']) self.end() if "additionalItems" in typedef: if isinstance(typedef['additionalItems'], bool) and typedef['additionalItems']: self.line("**Items are limited to the list above_**") elif isinstance(typedef['additionalItems'], list): self.begin_list() for x in typedef['additionalItems']: self._write_link_or_fragment(x) self.end_list() else: self._write_link_or_fragment(typedef['additionalItems']) if "minItems" in typedef: self.define("Min items", typedef['minItems']) if "maxItems" in typedef: self.define("Max items", typedef['maxItems']) if typedef.get("uniqueItems", False): self.line("**All items must be unique**") def _write_numeric(self, typedef): if "multipleOf" in typedef: self.define("Must be a multiple of", typedef['multipleOf']) if "minimum" in typedef: self.define("Minimum", typedef['minimum']) if "exclusiveMinimum" in typedef: self.define("Greater than", typedef['exclusiveMinimum']) if "maximum" in typedef: self.define("Maximum", typedef['maximum']) if "exclusiveMaximum" in typedef: self.define("Less than", typedef['exclusiveMaximum']) def _write_string(self, typedef): if "minLength" in typedef: self.define("Min length", typedef['minLength']) if "maxLength" in typedef: self.define("Max length", typedef['maxLength']) if "pattern" in typedef: self.define("Matches regular expression", "``" + typedef['pattern'] + "``") if "format" in typedef: self.define("Required format", typedef['format']) def _type_specifier(self, typedef): if "type" in typedef: if 'refersTo' in typedef: return typedef['refersTo'] else: return typedef['type'] elif "$ref" in typedef: r = typedef["$ref"].rsplit('/', 1)[-1] if r.startswith('#'): r = r[1:] return '`' + r + '`_' elif "enum" in typedef and len(typedef['enum']): value = typedef["enum"][0] if isinstance(value, dict): return 'object' elif isinstance(value, list): return 'array' elif isinstance(value, int): return 'number' else: return 'string' elif "allOf" in typedef: return "All:" + ', '.join(self._type_specifier(x) for x in typedef['allOf']) elif "anyOf" in typedef: return "Any:" + ', '.join(self._type_specifier(x) for x in typedef['anyOf']) elif "oneOf" in typedef: return "One:" + ', '.join(self._type_specifier(x) for x in typedef['oneOf']) return None def _validation_specifiers(self, typedef): """Todo: write out JSON validation specifiers as a new ul""" if "enum" in typedef: self.define("Valid values", json.dumps(typedef["enum"])) if "allOf" in typedef: self.begin("Inherits") self.begin_list() for frag in typedef['allOf']: self._write_link_or_fragment(frag) self.end_list() self.end() if "anyOf" in typedef: self.begin("Any of") self.begin_list() for frag in typedef['anyOf']: self._write_link_or_fragment(frag) self.end_list() self.end() if "oneOf" in typedef: self.begin("One of") self.begin_list() for frag in typedef['oneOf']: self._write_link_or_fragment(frag) self.end_list() self.end() if "not" in typedef: self.begin("Not") self._validation_specifiers(typedef["not"]) self.end() def _metadata_keywords(self, typedef): if "title" in typedef: pass if "default" in typedef: self.define("Default value", typedef['default']) if "description" in typedef: pass def _link_name(self, reference): try: target = self._refs[reference] if "title" in target: return target["title"] else: return reference.rsplit('/', 1)[-1] except KeyError: return reference def _deref(self, reference): if reference in self._refs: link_name = self._link_name(reference) else: link_name = reference return "`{link_name}`_".format(link_name=link_name) def _description(self, typedef): if "description" in typedef: if "title" in typedef: return "__{title}__. {description}".format(**typedef) else: return typedef['description'] elif "title" in typedef: return typedef['title'] else: return "*No description provided.*"