Example #1
0
 def test_annotations_bad_tag(self):
     """Replace a good tag with a bad one and get a syntax error."""
     docstring = self.sample_api_annotated_docstring
     api_docstring_parser = APIDocstringParser()
     api_docstring_parser.parse(docstring.replace("@param", "@bad"))
     d = api_docstring_parser.get_dict()
     self.assert_has_syntax_error(d)
Example #2
0
def describe_actions(handler):
    """Describe the actions that `handler` supports.

    For each action, which could be a CRUD operation or a custom (piggybacked)
    operation, a dict with the following entries is generated:

      method: string, HTTP method.
      name: string, a human-friendly name for the action.
      doc: string, documentation about the action.
      op: string or None, the op parameter to pass in requests for
          non-CRUD/ReSTful requests.
      restful: Indicates if this is a CRUD/ReSTful action.

    """
    from maasserver.api import support  # Circular import.
    getname = support.OperationsResource.crudmap.get
    for signature, function in handler.exports.items():
        http_method, operation = signature
        name = getname(http_method) if operation is None else operation

        ap = APIDocstringParser()
        doc = getdoc(function)

        if doc is not None:
            if APIDocstringParser.is_annotated_docstring(doc):
                # Because the docstring contains annotations, we
                # need to construct a string suitable for output
                # to stdout that matches the style used for
                # non-annotated docstrings in the CLI.
                ap.parse(doc)
                d = ap.get_dict()
                if d['description_title'] != "":
                    doc = d['description_title'] + "\n\n"
                    doc += d['description'] + "\n\n"
                else:
                    doc = d['description'] + "\n\n"

                # Here, we add the params, but we skip params
                # surrounded by curly brackets (e.g. {foo})
                # because these indicate params that appear in
                # the URI (e.g. /zone/{name}/). I.e. positional
                # arguments. These already appear in the CLI
                # help command output so we don't want duplicates.
                for p in d['params']:
                    if p['name'].find("{") == -1 and p['name'].find("}") == -1:
                        required = "Required. "
                        if p['options']['required'] == "false":
                            required = "Optional. "

                        doc += (":param %s: %s%s" %
                                (p['name'], required, p['description']))
                        doc += (":type %s: %s\n " % (p['name'], p['type']))

        yield dict(
            method=http_method, name=name, doc=doc,
            op=operation, restful=(operation is None))
Example #3
0
    def test_warn_on_missing_example_db_entry(self):
        """Ensure we see a warning if there is a missing examples db entry."""
        ds_orig = self.sample_api_annotated_docstring

        ds_bad_exkey = ds_orig.replace('"success_with_exdb" [exkey=key1]',
                                       '"success_with_exdb" [exkey=badkey]')

        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(ds_bad_exkey, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()

        self.assert_has_api_warning(d)
Example #4
0
    def test_parse_annotations_indent_descriptions(self):
        """Indentation should be kept when present in descriptions."""
        docstring = self.sample_api_annotated_docstring
        ref_string = "Longer description with\n" "    multiple lines.\n\n    "
        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(docstring, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()

        # Note that we only test one description here because the
        # same code is used to gather all description areas of the
        # tags. E.g. @tag (type) "name" [options] description
        self.assertEqual(d["description"], ref_string)
Example #5
0
    def test_parse_annotations(self):
        """Tests whether we can parse the sample."""

        docstring = self.sample_api_annotated_docstring
        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(docstring, "method", "uri", "operation")
        d = api_docstring_parser.get_dict()

        params = d['params']
        successes = d['successes']
        errors = d['errors']

        self.assertEqual(d['http_method'], "method")
        self.assertEqual(d['uri'], "uri")
        self.assertEqual(d['operation'], "operation")
        self.assertEqual(d['description_title'], "Docstring title")
        self.assertEqual(" ".join(d['description'].split()),
                         "Longer description with multiple lines.")

        p = params[0]
        self.assertEqual(p['type'], "String")
        self.assertEqual(p['name'], "param_name")
        self.assertEqual(" ".join(p['description'].split()),
                         "param description")
        self.assertEqual(" ".join(p['example'].split()), "param-ex")

        p = params[1]
        self.assertEqual(p['type'], "Int")
        self.assertEqual(p['name'], "param_name2")
        self.assertEqual(" ".join(p['description'].split()),
                         "param2 description")
        self.assertEqual(" ".join(p['example'].split()), "param2-ex")

        p = params[2]
        self.assertEqual(p['type'], "URL String")
        self.assertEqual(p['name'], "param_name3")
        self.assertEqual(" ".join(p['description'].split()),
                         "param3 description")
        self.assertEqual(" ".join(p['example'].split()), "param3-ex")

        s = successes[0]
        self.assertEqual(s['type'], "Content")
        self.assertEqual(s['name'], "success_name")
        self.assertEqual(" ".join(s['description'].split()),
                         "success description")
        self.assertEqual(" ".join(s['example'].split()), "success content")

        e = errors[0]
        self.assertEqual(e['type'], "HTTP Status Code")
        self.assertEqual(e['name'], "error_name")
        self.assertEqual(" ".join(e['description'].split()),
                         "error description")
        self.assertEqual(" ".join(e['example'].split()), "error content")
Example #6
0
    def test_template_renders_with_no_warnings(self):
        """The Tempita tmpl-api.rst template should render for the sample
        annotated API docstring with no errors.
        """
        ds = self.sample_api_annotated_docstring
        template = APITemplateRenderer()
        template_path = (
            "%s/../%s" %
            (os.path.dirname(__file__), self.api_tempita_template))

        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(ds, uri=self.test_uri_plural)

        result = template.apply_template(template_path, api_docstring_parser)

        self.assertThat(result, Not(Contains("API_WARNING")))
Example #7
0
    def test_parse_annotations_indent_example(self):
        """Indentation should be kept when present in examples."""
        docstring = self.sample_api_annotated_docstring
        ref_string = ("{\n"
                      "            \"id\": 1,\n"
                      "            \"foo\": \"bar\"\n"
                      "        }\n\n    ")
        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(docstring, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()

        # Note that we only test one example here because the
        # same code is used to gather all description areas of the
        # tags. E.g. @tag-example (type) "name" [options] description
        params = d['params']
        self.assertEqual(params[3]['example'], ref_string)
Example #8
0
    def test_find_examples_db(self):
        """Ensure parser correctly finds example databases."""
        ds = self.sample_api_annotated_docstring

        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(ds, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()

        s = d['successes'][1]

        self.assertEqual(" ".join(s['example'].split()), '{ "name": "value" }')

        api_docstring_parser.parse(ds, uri=self.test_uri_plural)
        d = api_docstring_parser.get_dict()

        s = d['successes'][1]

        self.assertEqual(" ".join(s['example'].split()), '{ "name": "value" }')
Example #9
0
    def test_warn_on_missing_example_db_when_entry_referenced(self):
        """Missing examples db.

        If an examples db does not exist for some given URI (like when it
        simply hasn't been created yet) and a key from that missing DB is
        referenced by the API, we should see a warning.

        Note that _not_ having an examples db for a particular operation is a
        normal and acceptable condition (it takes a while to create one). It
        only becomes an error condition when the API tries to reference
        something inside a non-existent examples database.
        """
        ds = self.sample_api_annotated_docstring

        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(ds, uri="bad_uri")
        d = api_docstring_parser.get_dict()

        self.assert_has_api_warning(d)
Example #10
0
    def test_load_nodes_examples_by_default(self):
        """Nodes examples should be loading by default.

        Some API objects like machines and devices inherit operations from
        nodes, so when we load the examples database, we always start with
        nodes and add on object-specific examples.
        """
        ds = self.sample_api_annotated_docstring

        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(ds, uri=self.test_uri_plural)
        d = api_docstring_parser.get_dict()

        # index=2 contains the example with inherited examples from
        # example/nodes.json
        s = d['successes'][2]

        # The presence of the 'resource-uri' string is a good indicator
        # that the 'read-node' key has picked up the JSON object and
        # converted it to a string for output in API docs.
        self.assertTrue(s['example'].find("resource_uri") != -1)
Example #11
0
    def test_annotations_orphaned_example_tags(self):
        """Tests to ensure orphaned examples are found.

        Orphaned examples are example tags that have no matching
        non-example tag: E.g. param/param-example. The name field
        is used to determine matches.
        """
        docstring = self.sample_api_annotated_docstring
        api_docstring_parser = APIDocstringParser()
        docstring = docstring.replace("@param-example \"param_name\"",
                                      "@param-example \"param_name_bad\"")
        api_docstring_parser.parse(docstring, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()
        self.assert_has_api_warning(d)

        docstring = docstring.replace("@error-example \"error_name\"",
                                      "@error-example \"error_name_bad\"")
        api_docstring_parser.parse(docstring, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()
        self.assert_has_api_warning(d)

        docstring = docstring.replace("@success-example \"success_name\"",
                                      "@success-example \"success_name_bad\"")
        api_docstring_parser.parse(docstring, uri=self.test_uri_singular)
        d = api_docstring_parser.get_dict()
        self.assert_has_api_warning(d)
Example #12
0
    def test_parse_annotations(self):
        """Tests whether we can parse the sample."""

        docstring = self.sample_api_annotated_docstring
        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(docstring,
                                   http_method="mymethod",
                                   uri=self.test_uri_singular,
                                   operation="myoperation")
        d = api_docstring_parser.get_dict()

        params = d['params']
        successes = d['successes']
        errors = d['errors']

        self.assertEqual(d['http_method'], "mymethod")
        self.assertEqual(d['uri'], self.test_uri_singular)
        self.assertEqual(d['operation'], "myoperation")
        self.assertEqual(d['description_title'], "Docstring title")
        self.assertEqual(" ".join(d['description'].split()),
                         "Longer description with multiple lines.")

        p = params[0]
        self.assertEqual(p['type'], "String")
        self.assertEqual(p['name'], "param_name")
        self.assertEqual(" ".join(p['description'].split()),
                         "param description")
        self.assertEqual(" ".join(p['example'].split()), "param-ex")

        p = params[1]
        self.assertEqual(p['type'], "Int")
        self.assertEqual(p['name'], "param_name2")
        self.assertEqual(" ".join(p['description'].split()),
                         "param2 description")
        self.assertEqual(" ".join(p['example'].split()), "param2-ex")

        p = params[2]
        self.assertEqual(p['type'], "URL String")
        self.assertEqual(p['name'], "param_name3")
        self.assertEqual(" ".join(p['description'].split()),
                         "param3 description")
        self.assertEqual(" ".join(p['example'].split()), "param3-ex")

        p = params[3]
        self.assertEqual(p['type'], "JSON")
        self.assertEqual(p['name'], "param_name4")
        self.assertEqual(" ".join(p['description'].split()),
                         "param4 description")
        self.assertEqual(" ".join(p['example'].split()),
                         "{ \"id\": 1, \"foo\": \"bar\" }")

        p = params[4]
        self.assertEqual(p['type'], "Boolean")
        self.assertEqual(p['name'], "param_name5")
        self.assertEqual(" ".join(p['description'].split()),
                         "param5 description")
        self.assertEqual(" ".join(p['example'].split()), "True")

        p = params[5]
        self.assertEqual(p['type'], "Float")
        self.assertEqual(p['name'], "param_name6")
        self.assertEqual(" ".join(p['description'].split()),
                         "param6 description")
        self.assertEqual(" ".join(p['example'].split()), "1.5")

        s = successes[0]
        self.assertEqual(s['type'], "Content")
        self.assertEqual(s['name'], "success_name")
        self.assertEqual(" ".join(s['description'].split()),
                         "success description")
        self.assertEqual(" ".join(s['example'].split()), "success content")

        e = errors[0]
        self.assertEqual(e['type'], "HTTP Status Code")
        self.assertEqual(e['name'], "error_name")
        self.assertEqual(" ".join(e['description'].split()),
                         "error description")
        self.assertEqual(" ".join(e['example'].split()), "error content")
Example #13
0
def render_api_docs():
    """Render ReST documentation for the REST API.


    This module's docstring forms the head of the documentation; details of
    the API methods follow.

    :return: Documentation, in ReST, for the API.
    :rtype: :class:`unicode`
    """
    from maasserver import urls_api as urlconf

    module = sys.modules[__name__]
    output = StringIO()
    line = partial(print, file=output)

    line(getdoc(module))
    line()
    line()
    line("Operations")
    line("``````````")
    line()

    def export_key(export):
        """Return a sortable key for an export.

        `op` is often `None`, which cannot be compared to non-`None`
        operations.
        """
        (http_method, op), function = export
        if op is None:
            return http_method, "", function
        else:
            return http_method, op, function

    annotation_parser = APIDocstringParser()
    templates = APITemplateRenderer()
    resources = find_api_resources(urlconf)
    for doc in generate_api_docs(resources):
        uri_template = doc.resource_uri_template
        exports = doc.handler.exports.items()
        # Derive a section title from the name of the handler class.
        section_name = doc.handler.api_doc_section_name
        line(section_name)
        line("=" * len(section_name))
        # Note:
        # The following dedent is useless in the following situation:
        #
        # def somefunc(foo)
        #     """No indent here
        #
        #     Here, there is an indent, so dedent doesn't do
        #     anything.
        #    """
        #
        # This fixes the problem:
        #
        # def somefunc(foo)
        #     """
        #     Indent here
        #
        #     Now dedent works because the entire docstring appears
        #     to be indented.
        #    """
        #
        # This also works because the dedent version is the same
        # as the non-dented version:
        #
        # def somefunc(foo)
        #     """No indent here"""
        #
        line(dedent(doc.handler.__doc__).strip())
        line()
        line()
        for (http_method, op), function in sorted(exports, key=export_key):
            operation = " op=%s" % op if op is not None else ""
            subsection = "``%s %s%s``" % (http_method, uri_template, operation)
            docstring = getdoc(function)
            if docstring is not None:
                if APIDocstringParser.is_annotated_docstring(docstring):
                    operation = "op=%s" % op if op is not None else ""
                    annotation_parser.parse(
                        docstring, http_method, uri_template, operation
                    )
                    line(
                        templates.apply_template(
                            os.path.dirname(__file__) + "/tmpl-apidoc.rst",
                            annotation_parser,
                        )
                    )
                else:
                    line("%s\n%s\n" % (subsection, "#" * len(subsection)))
                    line()
                    for docline in dedent(docstring).splitlines():
                        if docline.strip() == "":
                            # Blank line.  Don't indent.
                            line()
                        else:
                            # Print documentation line, indented.
                            line(docline)
                line()
            else:
                line("%s\n%s\n" % (subsection, "#" * len(subsection)))
                line()

    line()
    line()
    line(generate_power_types_doc())
    line()
    line()
    line(generate_pod_types_doc())

    return output.getvalue()
Example #14
0
    def test_parse_annotations(self):
        """Tests whether we can parse the sample."""

        docstring = self.sample_api_annotated_docstring
        api_docstring_parser = APIDocstringParser()
        api_docstring_parser.parse(
            docstring,
            http_method="mymethod",
            uri=self.test_uri_singular,
            operation="myoperation",
        )
        d = api_docstring_parser.get_dict()

        params = d["params"]
        successes = d["successes"]
        errors = d["errors"]

        self.assertEqual(d["http_method"], "mymethod")
        self.assertEqual(d["uri"], self.test_uri_singular)
        self.assertEqual(d["operation"], "myoperation")
        self.assertEqual(d["description_title"], "Docstring title")
        self.assertEqual(
            " ".join(d["description"].split()),
            "Longer description with multiple lines.",
        )

        p = params[0]
        self.assertEqual(p["type"], "String")
        self.assertEqual(p["name"], "param_name")
        self.assertEqual(
            " ".join(p["description"].split()), "param description"
        )
        self.assertEqual(" ".join(p["example"].split()), "param-ex")

        p = params[1]
        self.assertEqual(p["type"], "Int")
        self.assertEqual(p["name"], "param_name2")
        self.assertEqual(
            " ".join(p["description"].split()), "param2 description"
        )
        self.assertEqual(" ".join(p["example"].split()), "param2-ex")

        p = params[2]
        self.assertEqual(p["type"], "URL String")
        self.assertEqual(p["name"], "param_name3")
        self.assertEqual(
            " ".join(p["description"].split()), "param3 description"
        )
        self.assertEqual(" ".join(p["example"].split()), "param3-ex")

        p = params[3]
        self.assertEqual(p["type"], "JSON")
        self.assertEqual(p["name"], "param_name4")
        self.assertEqual(
            " ".join(p["description"].split()), "param4 description"
        )
        self.assertEqual(
            " ".join(p["example"].split()), '{ "id": 1, "foo": "bar" }'
        )

        p = params[4]
        self.assertEqual(p["type"], "Boolean")
        self.assertEqual(p["name"], "param_name5")
        self.assertEqual(
            " ".join(p["description"].split()), "param5 description"
        )
        self.assertEqual(" ".join(p["example"].split()), "True")

        p = params[5]
        self.assertEqual(p["type"], "Float")
        self.assertEqual(p["name"], "param_name6")
        self.assertEqual(
            " ".join(p["description"].split()), "param6 description"
        )
        self.assertEqual(" ".join(p["example"].split()), "1.5")

        s = successes[0]
        self.assertEqual(s["type"], "Content")
        self.assertEqual(s["name"], "success_name")
        self.assertEqual(
            " ".join(s["description"].split()), "success description"
        )
        self.assertEqual(" ".join(s["example"].split()), "success content")

        e = errors[0]
        self.assertEqual(e["type"], "HTTP Status Code")
        self.assertEqual(e["name"], "error_name")
        self.assertEqual(
            " ".join(e["description"].split()), "error description"
        )
        self.assertEqual(" ".join(e["example"].split()), "error content")