diff --git a/doc/source/conf.py b/doc/source/conf.py index 05443eb3..f31d3b96 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -18,6 +18,7 @@ import sys import openstackdocstheme sys.path.insert(0, os.path.abspath('../..')) +sys.path.insert(0, os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be @@ -25,8 +26,12 @@ sys.path.insert(0, os.path.abspath('../..')) extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'enforcer' ] +# When True, this will raise an exception that kills sphinx-build. +enforcer_warnings_as_errors = False + # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable diff --git a/doc/source/enforcer.py b/doc/source/enforcer.py new file mode 100644 index 00000000..f75f9a30 --- /dev/null +++ b/doc/source/enforcer.py @@ -0,0 +1,113 @@ +import importlib +import os + +from bs4 import BeautifulSoup +from sphinx import errors + +# NOTE: We do this because I can't find any way to pass "-v" +# into sphinx-build through pbr... +DEBUG = True if os.getenv("ENFORCER_DEBUG") else False + +WRITTEN_METHODS = set() + + +class EnforcementError(errors.SphinxError): + """A mismatch between what exists and what's documented""" + category = "Enforcer" + + +def get_proxy_methods(): + """Return a set of public names on all proxies""" + names = ["openstack.bare_metal.v1._proxy", + "openstack.block_store.v2._proxy", + "openstack.cluster.v1._proxy", + "openstack.compute.v2._proxy", + "openstack.database.v1._proxy", + "openstack.identity.v2._proxy", + "openstack.identity.v3._proxy", + "openstack.image.v1._proxy", + "openstack.image.v2._proxy", + "openstack.key_manager.v1._proxy", + "openstack.message.v1._proxy", + "openstack.message.v2._proxy", + "openstack.metric.v1._proxy", + "openstack.network.v2._proxy", + "openstack.object_store.v1._proxy", + "openstack.orchestration.v1._proxy", + "openstack.telemetry.v2._proxy", + "openstack.telemetry.alarm.v2._proxy", + "openstack.workflow.v2._proxy"] + + modules = (importlib.import_module(name) for name in names) + + methods = set() + for module in modules: + # We're not going to use the Proxy for anything other than a `dir` + # so just pass a dummy value so we can create the instance. + instance = module.Proxy("") + # We only document public names + names = [name for name in dir(instance) if not name.startswith("_")] + good_names = [module.__name__ + ".Proxy." + name for name in names] + methods.update(good_names) + + return methods + + +def page_context(app, pagename, templatename, context, doctree): + """Handle html-page-context-event + + This event is emitted once the builder has the contents to create + an HTML page, but before the template is rendered. This is the point + where we'll know what documentation is going to be written, so + gather all of the method names that are about to be included + so we can check which ones were or were not processed earlier + by autodoc. + """ + if "users/proxies" in pagename: + soup = BeautifulSoup(context["body"], "html.parser") + dts = soup.find_all("dt") + ids = [dt.get("id") for dt in dts] + + written = 0 + for id in ids: + if id is not None and "_proxy.Proxy" in id: + WRITTEN_METHODS.add(id) + written += 1 + + if DEBUG: + app.info("ENFORCER: Wrote %d proxy methods for %s" % ( + written, pagename)) + + +def build_finished(app, exception): + """Handle build-finished event + + This event is emitted once the builder has written all of the output. + At this point we just compare what we know was written to what we know + exists within the modules and share the results. + + When enforcer_warnings_as_errors=True in conf.py, this method + will raise EnforcementError on any failures in order to signal failure. + """ + all_methods = get_proxy_methods() + + app.info("ENFORCER: %d proxy methods exist" % len(all_methods)) + app.info("ENFORCER: %d proxy methods written" % len(WRITTEN_METHODS)) + missing = all_methods - WRITTEN_METHODS + missing_count = len(missing) + app.info("ENFORCER: Found %d missing proxy methods " + "in the output" % missing_count) + + for name in sorted(missing): + app.warn("ENFORCER: %s was not included in the output" % name) + + if app.config.enforcer_warnings_as_errors: + raise EnforcementError( + "There are %d undocumented proxy methods" % missing_count) + + +def setup(app): + app.add_config_value("enforcer_warnings_as_errors", False, "env") + + app.connect("html-page-context", page_context) + app.connect("build-finished", build_finished) diff --git a/test-requirements.txt b/test-requirements.txt index a42add4f..3a561bba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ # process, which may cause wedges in the gate later. hacking<0.11,>=0.10.0 +beautifulsoup4 # MIT coverage>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD mock>=2.0 # BSD