pprint jsonschema in plugin reference
Change-Id: I317d60391556a362b93c5a1b769d7cd44fdae5ac
This commit is contained in:
@@ -13,14 +13,174 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
from docutils.parsers import rst
|
||||
import json
|
||||
import re
|
||||
|
||||
from rally.common.plugin import discover
|
||||
from rally.common.plugin import plugin
|
||||
from rally import plugins
|
||||
from utils import category, subcategory, section, paragraph, parse_text, \
|
||||
make_definition
|
||||
make_definitions, note
|
||||
|
||||
|
||||
JSON_SCHEMA_TYPES_MAP = {"boolean": "bool",
|
||||
"string": "str",
|
||||
"number": "float",
|
||||
"integer": "int",
|
||||
"array": "list",
|
||||
"object": "dict"}
|
||||
|
||||
|
||||
def process_jsonschema(schema):
|
||||
"""Process jsonschema and make it looks like regular docstring."""
|
||||
|
||||
if not schema:
|
||||
# nothing to parse
|
||||
return
|
||||
|
||||
if "type" in schema:
|
||||
|
||||
# str
|
||||
if schema["type"] == "string":
|
||||
doc = schema.get("description", "")
|
||||
if "pattern" in schema:
|
||||
doc += ("\n\nShould follow next pattern: %s." %
|
||||
schema["pattern"])
|
||||
return {"doc": doc, "type": "str"}
|
||||
|
||||
# int or float
|
||||
elif schema["type"] in ("integer", "number"):
|
||||
doc = schema.get("description", "")
|
||||
if "minimum" in schema:
|
||||
doc += "\n\nMin value: %s." % schema["minimum"]
|
||||
if "maximum" in schema:
|
||||
doc += "\n\nMax value: %s." % schema["maximum"]
|
||||
return {"doc": doc, "type": JSON_SCHEMA_TYPES_MAP[schema["type"]]}
|
||||
|
||||
# bool or null
|
||||
elif schema["type"] in ("boolean", "null"):
|
||||
return {"doc": schema.get("description", ""),
|
||||
"type": "bool" if schema["type"] == "boolean" else "null"}
|
||||
|
||||
# list
|
||||
elif schema["type"] == "array":
|
||||
info = {"doc": schema.get("description", ""),
|
||||
"type": "list"}
|
||||
|
||||
if "items" in schema:
|
||||
if info["doc"]:
|
||||
info["doc"] += "\n\n"
|
||||
info["doc"] += ("Elements of the list should follow format(s) "
|
||||
"described below:\n\n")
|
||||
|
||||
items = schema["items"]
|
||||
if "type" in items:
|
||||
itype = JSON_SCHEMA_TYPES_MAP.get(items["type"],
|
||||
items["type"])
|
||||
info["doc"] += "- Type: %s. " % itype
|
||||
if "description" in items:
|
||||
# add indention
|
||||
desc = items["description"].split("\n")
|
||||
info["doc"] += "\n ".join(desc)
|
||||
|
||||
if itype in ("list", "dict"):
|
||||
new_schema = copy.copy(items)
|
||||
new_schema.pop("description", None)
|
||||
new_schema = json.dumps(new_schema, indent=4)
|
||||
new_schema = "\n ".join(
|
||||
new_schema.split("\n"))
|
||||
|
||||
info["doc"] += ("\n Format:\n\n"
|
||||
" .. code-block:: json\n\n"
|
||||
" %s\n" % new_schema)
|
||||
else:
|
||||
info["doc"] += " - ``%s`` " % items
|
||||
return info
|
||||
|
||||
elif isinstance(schema["type"], list):
|
||||
# it can be too complicated for parsing... do not do it deeply
|
||||
return {"doc": schema.get("description", ""),
|
||||
"type": "/".join(schema["type"])}
|
||||
|
||||
# dict
|
||||
elif schema["type"] == "object":
|
||||
info = {"doc": schema.get("description", ""),
|
||||
"type": "dict",
|
||||
"parameters": []}
|
||||
required_parameters = schema.get("required", [])
|
||||
if "properties" in schema:
|
||||
for name in schema["properties"]:
|
||||
if isinstance(schema["properties"][name], str):
|
||||
pinfo = {"name": name,
|
||||
"type": schema["properties"][name],
|
||||
"doc": ""}
|
||||
else:
|
||||
pinfo = process_jsonschema(schema["properties"][name])
|
||||
if name in required_parameters:
|
||||
pinfo["required"] = True
|
||||
pinfo["name"] = name
|
||||
info["parameters"].append(pinfo)
|
||||
elif "patternProperties" in schema:
|
||||
info.pop("parameters", None)
|
||||
info["patternProperties"] = []
|
||||
for k, v in schema["patternProperties"].items():
|
||||
info["patternProperties"].append(process_jsonschema(v))
|
||||
info["patternProperties"][-1]["name"] = k
|
||||
info["patternProperties"][-1]["type"] = "str"
|
||||
elif (not (set(schema.keys()) - {"type", "description", "$schema",
|
||||
"additionalProperties"})):
|
||||
# it is ok, schema accepts any object. nothing to add more
|
||||
pass
|
||||
elif "oneOf" in schema:
|
||||
# Example:
|
||||
# SCHEMA = {"type": "object", "$schema": consts.JSON_SCHEMA,
|
||||
# "oneOf": [{"properties": {"foo": {"type": "string"}}
|
||||
# "required": ["foo"],
|
||||
# "additionalProperties": False},
|
||||
# {"properties": {"bar": {"type": "string"}}
|
||||
# "required": ["bar"],
|
||||
# "additionalProperties": False},
|
||||
#
|
||||
oneOf = copy.deepcopy(schema["oneOf"])
|
||||
for item in oneOf:
|
||||
for k, v in schema.items():
|
||||
if k not in ("oneOf", "description"):
|
||||
item[k] = v
|
||||
|
||||
return {"doc": schema.get("description", ""),
|
||||
"type": "dict",
|
||||
"oneOf": [process_jsonschema(item) for item in oneOf]}
|
||||
else:
|
||||
raise Exception("Failed to parse jsonschema: %s" % schema)
|
||||
|
||||
if "definitions" in schema:
|
||||
info["definitions"] = schema["definitions"]
|
||||
return info
|
||||
else:
|
||||
raise Exception("Failed to parse jsonschema: %s" % schema)
|
||||
|
||||
# enum
|
||||
elif "enum" in schema:
|
||||
doc = schema.get("description", "")
|
||||
doc += "\nSet of expected values: '%s'." % ("', '".join(
|
||||
[e or "None" for e in schema["enum"]]))
|
||||
return {"doc": doc}
|
||||
|
||||
elif "anyOf" in schema:
|
||||
return {"doc": schema.get("description", ""),
|
||||
"anyOf": [process_jsonschema(i) for i in schema["anyOf"]]}
|
||||
|
||||
elif "oneOf" in schema:
|
||||
return {"doc": schema.get("description", ""),
|
||||
"oneOf": [process_jsonschema(i) for i in schema["oneOf"]]}
|
||||
|
||||
elif "$ref" in schema:
|
||||
return {"doc": schema.get("description", "n/a"),
|
||||
"ref": schema["$ref"]}
|
||||
else:
|
||||
raise Exception("Failed to parse jsonschema: %s" % schema)
|
||||
|
||||
|
||||
CATEGORIES = {
|
||||
@@ -32,6 +192,7 @@ CATEGORIES = {
|
||||
"Verification Component": ["Verifier Context", "Verification Reporter",
|
||||
"Verifier Manager"]
|
||||
}
|
||||
|
||||
# NOTE(andreykurilin): several bases do not have docstings at all, so it is
|
||||
# redundant to display them
|
||||
IGNORED_BASES = ["Resource Type", "Task Exporter", "OS Client"]
|
||||
@@ -41,20 +202,18 @@ class PluginsReferenceDirective(rst.Directive):
|
||||
optional_arguments = 1
|
||||
option_spec = {"base_cls": str}
|
||||
|
||||
@staticmethod
|
||||
def _make_pretty_parameters(parameters, ref_prefix):
|
||||
if not parameters:
|
||||
return []
|
||||
|
||||
results = [paragraph("**Parameters**:")]
|
||||
for p in parameters:
|
||||
pname = p["name"]
|
||||
ref = ("%s%s" % (ref_prefix, pname)).lower().replace(".", "-")
|
||||
if "type" in p:
|
||||
pname += " (%s)" % p["type"]
|
||||
pdoc = "\n ".join(p["doc"].split("\n"))
|
||||
results.extend(make_definition(pname, ref, [pdoc]))
|
||||
return results
|
||||
def _make_arg_items(self, items, ref_prefix, description=None,
|
||||
title="Parameters"):
|
||||
terms = []
|
||||
for item in items:
|
||||
iname = item.get("name", "") or item.pop("type")
|
||||
if "type" in item:
|
||||
iname += " (%s)" % item["type"]
|
||||
terms.append((iname, [item["doc"]]))
|
||||
return make_definitions(title=title,
|
||||
ref_prefix=ref_prefix,
|
||||
terms=terms,
|
||||
descriptions=description)
|
||||
|
||||
def _make_plugin_section(self, plugin_cls, base_name=None):
|
||||
section_name = plugin_cls.get_name()
|
||||
@@ -73,18 +232,50 @@ class PluginsReferenceDirective(rst.Directive):
|
||||
section_obj.append(paragraph(
|
||||
"**Namespace**: %s" % info["namespace"]))
|
||||
|
||||
if base_name:
|
||||
ref_prefix = "%s-%s-" % (base_name, plugin_cls.get_name())
|
||||
else:
|
||||
ref_prefix = "%s-" % plugin_cls.get_name()
|
||||
|
||||
if info["parameters"]:
|
||||
if base_name:
|
||||
ref_prefix = "%s-%s-" % (base_name, plugin_cls.get_name())
|
||||
section_obj.extend(self._make_arg_items(info["parameters"],
|
||||
ref_prefix))
|
||||
|
||||
if info["returns"]:
|
||||
section_obj.extend(parse_text(
|
||||
"**Returns**:\n%s" % info["returns"]))
|
||||
|
||||
if info["schema"]:
|
||||
schema = process_jsonschema(info["schema"])
|
||||
if "type" in schema:
|
||||
if "parameters" in schema:
|
||||
section_obj.extend(self._make_arg_items(
|
||||
items=schema["parameters"],
|
||||
ref_prefix=ref_prefix))
|
||||
elif "patternProperties" in schema:
|
||||
section_obj.extend(self._make_arg_items(
|
||||
items=schema["patternProperties"],
|
||||
ref_prefix=ref_prefix,
|
||||
description=["*Dictionary is expected. Keys should "
|
||||
"follow pattern(s) described bellow.*"]))
|
||||
elif "oneOf" in schema:
|
||||
section_obj.append(note("One of the following groups of "
|
||||
"parameters should be provided."))
|
||||
for i, oneOf in enumerate(schema["oneOf"], 1):
|
||||
description = None
|
||||
if oneOf.get("doc", None):
|
||||
description = [oneOf["doc"]]
|
||||
section_obj.extend(self._make_arg_items(
|
||||
items=oneOf["parameters"],
|
||||
ref_prefix=ref_prefix,
|
||||
title="Option %s of parameters" % i,
|
||||
description=description))
|
||||
else:
|
||||
section_obj.extend(self._make_arg_items(
|
||||
items=[schema], ref_prefix=ref_prefix))
|
||||
else:
|
||||
ref_prefix = "%s-" % plugin_cls.get_name()
|
||||
|
||||
section_obj.extend(self._make_pretty_parameters(info["parameters"],
|
||||
ref_prefix))
|
||||
|
||||
if info["returns"]:
|
||||
section_obj.extend(parse_text(
|
||||
"**Returns**:\n%s" % info["returns"]))
|
||||
raise Exception("Failed to display provided schema: %s" %
|
||||
info["schema"])
|
||||
|
||||
filename = info["module"].replace(".", "/")
|
||||
ref = "https://github.com/openstack/rally/blob/master/%s.py" % filename
|
||||
@@ -100,6 +291,9 @@ class PluginsReferenceDirective(rst.Directive):
|
||||
else:
|
||||
subcategory_obj = []
|
||||
for p in sorted(base_cls.get_all(), key=lambda o: o.get_name()):
|
||||
# do not display hidden contexts
|
||||
if p._meta_get("hidden", False):
|
||||
continue
|
||||
subcategory_obj.append(self._make_plugin_section(p, base_name))
|
||||
|
||||
return subcategory_obj
|
||||
|
@@ -42,7 +42,7 @@ section = lambda title: parse_text("%s\n%s" % (title, "\"" * len(title)))[0]
|
||||
|
||||
|
||||
def make_definition(term, ref, descriptions):
|
||||
"""Constructs definition with reference to it"""
|
||||
"""Constructs definition with reference to it."""
|
||||
ref = ref.replace("_", "-").replace(" ", "-")
|
||||
definition = parse_text(
|
||||
".. _%(ref)s:\n\n* *%(term)s* [ref__]\n\n__ #%(ref)s" %
|
||||
@@ -55,3 +55,30 @@ def make_definition(term, ref, descriptions):
|
||||
descr = paragraph(" %s" % descr)
|
||||
definition.append(descr)
|
||||
return definition
|
||||
|
||||
|
||||
def make_definitions(title, ref_prefix, terms, descriptions=None):
|
||||
"""Constructs a list of definitions with reference to them."""
|
||||
raw_text = ["**%s**:" % title]
|
||||
if descriptions:
|
||||
for descr in descriptions:
|
||||
raw_text.append(descr)
|
||||
|
||||
for term, definitions in terms:
|
||||
ref = ("%s%s" % (ref_prefix, term)).lower().replace(
|
||||
".", "-").replace("_", "-").replace(" ", "-")
|
||||
raw_text.append(".. _%s:" % ref)
|
||||
raw_text.append("* *%s* [ref__]" % term)
|
||||
|
||||
for d in definitions:
|
||||
d = d.strip() if d else None
|
||||
if d:
|
||||
if d[0] not in string.ascii_uppercase:
|
||||
# .capitalize() removes existing caps
|
||||
d = d[0].upper() + d[1:]
|
||||
d = "\n ".join(d.split("\n"))
|
||||
raw_text.append(" %s" % d)
|
||||
|
||||
raw_text.append("__ #%s" % ref)
|
||||
|
||||
return parse_text("\n\n".join(raw_text) + "\n")
|
||||
|
Reference in New Issue
Block a user