diff --git a/doc/ext/plugin_reference.py b/doc/ext/plugin_reference.py index 67610039..5ac734ed 100644 --- a/doc/ext/plugin_reference.py +++ b/doc/ext/plugin_reference.py @@ -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 diff --git a/doc/ext/utils.py b/doc/ext/utils.py index 2bd53782..634822a4 100644 --- a/doc/ext/utils.py +++ b/doc/ext/utils.py @@ -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")