def test_resource_uri_in_docs_matches_handlers_idea_of_resource_uri(self): # Sigh. Piston asks handlers for resource_uri information, but also # makes use of Django's URL patterns to figure out resource_uri # templates for the documentation. Here we check that they match up. formatter = string.Formatter() def gen_handlers(resource): if resource.anonymous is not None: yield resource.anonymous if resource.handler is not None: yield resource.handler handlers = chain.from_iterable( map(gen_handlers, find_api_resources(urlconf)) ) mismatches = [] for handler in map(type, handlers): if hasattr(handler, "resource_uri"): resource_uri_params = handler.resource_uri()[1] resource_uri_template = generate_doc( handler ).resource_uri_template fields_expected = tuple(resource_uri_params) fields_observed = tuple( fname for _, fname, _, _ in formatter.parse( resource_uri_template ) if fname is not None ) if fields_observed != fields_expected: mismatches.append( (handler, fields_expected, fields_observed) ) if len(mismatches) != 0: messages = ( "{handler.__module__}.{handler.__name__} has mismatched " "fields:\n expected: {expected}\n observed: {observed}" "".format( handler=handler, expected=" ".join(expected), observed=" ".join(observed), ) for handler, expected, observed in mismatches ) messages = chain( messages, [ "Amend the URL patterns for these handlers/resources so that " "the observed fields match what is expected." ], ) self.fail("\n--\n".join(messages))
def test_urlpatterns_with_resource(self): # Resources for handlers with resource_uri attributes are discovered # in a urlconf module and returned. The type of resource_uri is not # checked; it must only be present and not None. handler = type("\\m/", (BaseHandler,), {"resource_uri": True}) resource = Resource(handler) module = self.make_module() module.urlpatterns = [url("^metal", resource)] self.assertSetEqual({resource}, find_api_resources(module))
def test_nested_urlpatterns_with_handler(self): # Resources are found in nested urlconfs. handler = type("\\m/", (BaseHandler,), {"resource_uri": True}) resource = Resource(handler) module = self.make_module() submodule = self.make_module() submodule.urlpatterns = [url("^metal", resource)] module.urlpatterns = [url("^genre/", include(submodule))] self.assertSetEqual({resource}, find_api_resources(module))
def test_urlpatterns_with_resource_hidden(self): # Resources for handlers with resource_uri attributes are discovered # in a urlconf module and returned, unless hidden = True. handler = type( "\\m/", (BaseHandler,), {"resource_uri": True, "hidden": True} ) resource = Resource(handler) module = self.make_module() module.urlpatterns = [url("^metal", resource)] self.assertSetEqual(set(), find_api_resources(module))
def test_handlers_have_section_title(self): from maasserver import urls_api as urlconf resources = find_api_resources(urlconf) handlers = [] for doc in generate_api_docs(resources): handlers.append(doc.handler) handlers_missing_section_name = [ handler.__name__ for handler in handlers if not hasattr(handler, 'api_doc_section_name') ] self.assertEqual( [], handlers_missing_section_name, "%d handlers are missing an api_doc_section_name field." % len(handlers_missing_section_name))
def test_urlpatterns_with_resource_for_incomplete_handler(self): # Resources for handlers that don't specify resource_uri are ignored. module = self.make_module() module.urlpatterns = [url("^foo", BaseHandler)] self.assertSetEqual(set(), find_api_resources(module))
def test_urlpatterns_empty(self): # No resources are found in empty modules. module = self.make_module() module.urlpatterns = [] self.assertSetEqual(set(), find_api_resources(module))
def test_smoke(self): # Resources are found for the MAAS API. from maasserver import urls_api as urlconf self.assertNotEqual(set(), find_api_resources(urlconf))
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 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)) 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) line("%s\n%s\n" % (subsection, '#' * len(subsection))) line() docstring = getdoc(function) if docstring is not None: for docline in dedent(docstring).splitlines(): if docline.strip() == '': # Blank line. Don't indent. line() else: # Print documentation line, indented. line(docline) line() line() line() line(generate_power_types_doc()) line() line() line(generate_pod_types_doc()) return output.getvalue()
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()