f8909fc1d5
sphinx-feature-classification was declared python-3-only by change I0f3e9d980c25545a in release 1.0.1 (2020-04-07 10:02:33 +0000). It's never had six in requirements.txt despite importing six in two files. Now that consuming projects are all python-3-only, six is not being pulled in by some other project's requirements. So convert six usage to pure python 3. Change-Id: I1917fda331860332242774019ac7ca54512430e3
491 lines
16 KiB
Python
491 lines
16 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
This provides a sphinx extension able to render the support-matrix.ini
|
|
file into the developer documentation.
|
|
|
|
It is used via a single directive in the .rst file
|
|
|
|
.. support_matrix::
|
|
|
|
"""
|
|
import configparser
|
|
from os import path
|
|
import re
|
|
|
|
from docutils import nodes
|
|
from docutils.parsers import rst
|
|
from sphinx.util.fileutil import copy_asset
|
|
|
|
KEY_PATTERN = re.compile("[^a-zA-Z0-9_]")
|
|
DRIVER_PREFIX = "driver."
|
|
FEATURE_PREFIX = 'operation.'
|
|
DRIVER_NOTES_PREFIX = "driver-notes."
|
|
|
|
|
|
class Matrix(object):
|
|
"""Represents the entire support matrix for project drivers"""
|
|
|
|
def __init__(self, cfg):
|
|
self.drivers = self._set_drivers(cfg)
|
|
self.features = self._set_features(cfg)
|
|
|
|
@staticmethod
|
|
def _set_drivers(cfg):
|
|
drivers = {}
|
|
|
|
for section in cfg.sections():
|
|
if not section.startswith(DRIVER_PREFIX):
|
|
continue
|
|
|
|
title = cfg.get(section, "title")
|
|
|
|
link = None
|
|
if cfg.has_option(section, 'link'):
|
|
link = cfg.get(section, "link")
|
|
|
|
driver = Driver(title, link)
|
|
drivers[section] = driver
|
|
|
|
return drivers
|
|
|
|
def _set_features(self, cfg):
|
|
features = []
|
|
|
|
def _process_feature(section):
|
|
if not cfg.has_option(section, "title"):
|
|
raise Exception(
|
|
"'title' option missing in '[%s]' section" % section)
|
|
|
|
title = cfg.get(section, "title")
|
|
status = Feature.STATUS_OPTIONAL
|
|
group = None
|
|
|
|
if cfg.has_option(section, "status"):
|
|
# The value is a string "status(group)" where
|
|
# the 'group' part is optional
|
|
status, group = re.match(r'^([^(]+)(?:\(([^)]+)\))?$',
|
|
cfg.get(section, "status")).groups()
|
|
|
|
if status not in Feature.STATUS_ALL:
|
|
raise ValueError(
|
|
"'status' option value '%s' in ['%s']"
|
|
"section must be one of (%s)" %
|
|
(status, section,
|
|
", ".join(Feature.STATUS_ALL)))
|
|
|
|
cli = []
|
|
if cfg.has_option(section, "cli"):
|
|
cli = cfg.get(section, "cli")
|
|
|
|
api = None
|
|
if cfg.has_option(section, "api"):
|
|
api = cfg.get(section, "api")
|
|
|
|
notes = None
|
|
if cfg.has_option(section, "notes"):
|
|
notes = cfg.get(section, "notes")
|
|
return Feature(
|
|
section, title,
|
|
status=status, group=group, notes=notes, cli=cli, api=api)
|
|
|
|
def _process_implementation(section, option, feature):
|
|
if option not in self.drivers:
|
|
raise Exception(
|
|
"'%s' section is not declared in the "
|
|
"INI file." % (option))
|
|
|
|
status = cfg.get(section, option)
|
|
if status not in Implementation.STATUS_ALL:
|
|
raise ValueError(
|
|
"%s is set to %s in '[%s]' section but must be "
|
|
"one of (%s)" % (option, status, section, ", ".join(
|
|
Implementation.STATUS_ALL)))
|
|
|
|
option_notes = ''.join([DRIVER_NOTES_PREFIX,
|
|
option[len(DRIVER_PREFIX):]])
|
|
notes = None
|
|
if cfg.has_option(section, option_notes):
|
|
notes = cfg.get(section, option_notes)
|
|
|
|
impl = Implementation(status=status, notes=notes)
|
|
feature.implementations[option] = impl
|
|
|
|
return feature
|
|
|
|
for section in cfg.sections():
|
|
if not section.startswith(FEATURE_PREFIX):
|
|
continue
|
|
|
|
feature = _process_feature(section)
|
|
|
|
# Now we've got the basic feature details, we must process
|
|
# the backend driver implementation for each feature
|
|
for option in cfg.options(section):
|
|
if not option.startswith(DRIVER_PREFIX):
|
|
continue
|
|
|
|
_process_implementation(section, option, feature)
|
|
features.append(feature)
|
|
|
|
return features
|
|
|
|
|
|
class Feature(object):
|
|
STATUS_CHOICE = "choice"
|
|
STATUS_CONDITION = "condition"
|
|
STATUS_MANDATORY = "mandatory"
|
|
STATUS_OPTIONAL = "optional"
|
|
STATUS_MATURE = "mature"
|
|
STATUS_IMMATURE = "immature"
|
|
|
|
STATUS_ALL = [STATUS_MANDATORY, STATUS_OPTIONAL, STATUS_CHOICE,
|
|
STATUS_CONDITION, STATUS_MATURE, STATUS_IMMATURE]
|
|
|
|
def __init__(self, key, title, status=STATUS_OPTIONAL,
|
|
group=None, notes=None, cli=(), api=None):
|
|
self.key = key
|
|
self.title = title
|
|
self.status = status
|
|
self.group = group
|
|
self.notes = notes
|
|
self.cli = cli
|
|
self.api = api
|
|
|
|
self.implementations = {}
|
|
|
|
|
|
class Implementation(object):
|
|
STATUS_COMPLETE = "complete"
|
|
STATUS_PARTIAL = "partial"
|
|
STATUS_MISSING = "missing"
|
|
STATUS_UNKNOWN = "unknown"
|
|
|
|
STATUS_ALL = [STATUS_COMPLETE, STATUS_MISSING,
|
|
STATUS_PARTIAL, STATUS_UNKNOWN]
|
|
|
|
def __init__(self, status=STATUS_MISSING, notes=None):
|
|
self.status = status
|
|
self.notes = notes
|
|
|
|
|
|
STATUS_SYMBOLS = {
|
|
Implementation.STATUS_COMPLETE: u"\u2714",
|
|
Implementation.STATUS_MISSING: u"\u2716",
|
|
Implementation.STATUS_PARTIAL: u"\u2714",
|
|
Implementation.STATUS_UNKNOWN: u"?"
|
|
}
|
|
|
|
|
|
class Driver(object):
|
|
def __init__(self, title, link=None):
|
|
"""Driver object.
|
|
|
|
:param title: Human readable name for plugin
|
|
:param link: A URL to documentation about the driver.
|
|
"""
|
|
self.title = title
|
|
self.link = link
|
|
|
|
|
|
class Directive(rst.Directive):
|
|
|
|
# support-matrix.ini is the arg
|
|
required_arguments = 1
|
|
|
|
def run(self):
|
|
matrix = self._load_support_matrix()
|
|
return self._build_markup(matrix)
|
|
|
|
def _load_support_matrix(self):
|
|
"""Parse support-matrix.ini file.
|
|
|
|
Reads the support-matrix.ini file and populates an instance of the
|
|
Matrix class with all the data.
|
|
|
|
:returns: Matrix instance
|
|
"""
|
|
|
|
cfg = configparser.ConfigParser()
|
|
env = self.state.document.settings.env
|
|
fname = self.arguments[0]
|
|
rel_fpath, fpath = env.relfn2path(fname)
|
|
|
|
with open(fpath) as fp:
|
|
cfg.read_file(fp)
|
|
|
|
# This ensures that the docs are rebuilt whenever the
|
|
# .ini file changes
|
|
env.note_dependency(rel_fpath)
|
|
|
|
matrix = Matrix(cfg)
|
|
return matrix
|
|
|
|
def _build_markup(self, matrix):
|
|
"""Constructs the docutils content for the support matrix."""
|
|
content = []
|
|
self._build_summary(matrix, content)
|
|
self._build_details(matrix, content)
|
|
self._build_notes(content)
|
|
return content
|
|
|
|
@staticmethod
|
|
def _build_summary(matrix, content):
|
|
"""Constructs the content for the summary of the support matrix.
|
|
|
|
The summary consists of a giant table, with one row
|
|
for each feature, and a column for each backend
|
|
driver. It provides an 'at a glance' summary of the
|
|
status of each driver.
|
|
"""
|
|
|
|
summary_title = nodes.subtitle(text="Summary")
|
|
summary = nodes.table(classes=["sp_feature_cells"])
|
|
cols = len(matrix.drivers.keys())
|
|
|
|
# Add two columns for the Feature and Status columns.
|
|
cols += 2
|
|
|
|
summary_group = nodes.tgroup(cols=cols)
|
|
summary_body = nodes.tbody()
|
|
summary_head = nodes.thead()
|
|
|
|
for i in range(cols):
|
|
summary_group.append(nodes.colspec(colwidth=1))
|
|
summary_group.append(summary_head)
|
|
summary_group.append(summary_body)
|
|
summary.append(summary_group)
|
|
content.append(summary_title)
|
|
content.append(summary)
|
|
|
|
# This sets up all the column headers - two fixed
|
|
# columns for feature name & status
|
|
header = nodes.row()
|
|
blank = nodes.entry(classes=["sp_feature_cells"])
|
|
blank.append(nodes.emphasis(text="Feature"))
|
|
header.append(blank)
|
|
blank = nodes.entry(classes=["sp_feature_cells"])
|
|
blank.append(nodes.emphasis(text="Status"))
|
|
header.append(blank)
|
|
summary_head.append(header)
|
|
|
|
# then one column for each backend driver
|
|
impls = sorted(matrix.drivers,
|
|
key=lambda x: matrix.drivers[x].title)
|
|
for key in impls:
|
|
driver = matrix.drivers[key]
|
|
implcol = nodes.entry(classes=["sp_feature_cells"])
|
|
header.append(implcol)
|
|
if driver.link:
|
|
uri = driver.link
|
|
target_ref = nodes.reference("", refuri=uri)
|
|
target_txt = nodes.inline()
|
|
implcol.append(target_txt)
|
|
target_txt.append(target_ref)
|
|
target_ref.append(nodes.strong(text=driver.title))
|
|
else:
|
|
implcol.append(nodes.strong(text=driver.title))
|
|
|
|
# We now produce the body of the table, one row for
|
|
# each feature to report on
|
|
for feature in matrix.features:
|
|
item = nodes.row()
|
|
|
|
# the hyperlink driver name linking to details
|
|
feature_id = re.sub(KEY_PATTERN, "_", feature.key)
|
|
|
|
# first the fixed columns for title/status
|
|
key_col = nodes.entry(classes=["sp_feature_cells"])
|
|
item.append(key_col)
|
|
key_ref = nodes.reference(refid=feature_id)
|
|
key_txt = nodes.inline()
|
|
key_col.append(key_txt)
|
|
key_txt.append(key_ref)
|
|
key_ref.append(nodes.strong(text=feature.title))
|
|
|
|
status_col = nodes.entry(classes=["sp_feature_cells"])
|
|
item.append(status_col)
|
|
status_col.append(nodes.inline(
|
|
text=feature.status,
|
|
classes=["sp_feature_" + feature.status]))
|
|
|
|
# and then one column for each backend driver
|
|
for key in impls:
|
|
impl = feature.implementations[key]
|
|
impl_col = nodes.entry(classes=["sp_feature_cells"])
|
|
item.append(impl_col)
|
|
|
|
key_id = re.sub(KEY_PATTERN, "_",
|
|
"{}_{}".format(feature.key, key))
|
|
|
|
impl_ref = nodes.reference(refid=key_id)
|
|
impl_txt = nodes.inline()
|
|
impl_col.append(impl_txt)
|
|
impl_txt.append(impl_ref)
|
|
|
|
status = STATUS_SYMBOLS.get(impl.status, "")
|
|
|
|
impl_ref.append(nodes.literal(
|
|
text=status,
|
|
classes=["sp_impl_summary", "sp_impl_" + impl.status]))
|
|
|
|
summary_body.append(item)
|
|
|
|
def _build_details(self, matrix, content):
|
|
"""Constructs the content for the details of the support matrix."""
|
|
|
|
details_title = nodes.subtitle(text="Details")
|
|
details = nodes.bullet_list()
|
|
|
|
content.append(details_title)
|
|
content.append(details)
|
|
|
|
# One list entry for each feature we're reporting on
|
|
for feature in matrix.features:
|
|
item = nodes.list_item()
|
|
|
|
status = feature.status
|
|
if feature.group is not None:
|
|
status += "({})".format(feature.group)
|
|
|
|
feature_id = re.sub(KEY_PATTERN, "_", feature.key)
|
|
|
|
# Highlight the feature title name
|
|
item.append(nodes.strong(text=feature.title, ids=[feature_id]))
|
|
|
|
# Add maturity status
|
|
para = nodes.paragraph()
|
|
para.append(nodes.strong(text="Status: {}. ".format(status)))
|
|
item.append(para)
|
|
|
|
if feature.api is not None:
|
|
para = nodes.paragraph()
|
|
para.append(
|
|
nodes.strong(text="API Alias: {} ".format(feature.api)))
|
|
item.append(para)
|
|
|
|
if feature.cli:
|
|
item.append(self._create_cli_paragraph(feature))
|
|
|
|
if feature.notes is not None:
|
|
item.append(self._create_notes_paragraph(feature.notes))
|
|
|
|
para_divers = nodes.paragraph()
|
|
para_divers.append(nodes.strong(text="Driver Support:"))
|
|
# A sub-list giving details of each backend driver
|
|
impls = nodes.bullet_list()
|
|
keys = sorted(feature.implementations,
|
|
key=lambda x: matrix.drivers[x].title)
|
|
for key in keys:
|
|
driver = matrix.drivers[key]
|
|
impl = feature.implementations[key]
|
|
subitem = nodes.list_item()
|
|
|
|
key_id = re.sub(KEY_PATTERN, "_",
|
|
"{}_{}".format(feature.key, key))
|
|
|
|
subitem += [
|
|
nodes.strong(text="{}: ".format(driver.title)),
|
|
nodes.literal(text=impl.status,
|
|
classes=["sp_impl_{}".format(impl.status)],
|
|
ids=[key_id]),
|
|
]
|
|
|
|
if impl.notes is not None:
|
|
subitem.append(self._create_notes_paragraph(impl.notes))
|
|
|
|
impls.append(subitem)
|
|
|
|
para_divers.append(impls)
|
|
item.append(para_divers)
|
|
details.append(item)
|
|
|
|
@staticmethod
|
|
def _build_notes(content):
|
|
"""Constructs a list of notes content for the support matrix.
|
|
|
|
This is generated as a bullet list.
|
|
"""
|
|
notes_title = nodes.subtitle(text="Notes:")
|
|
notes = nodes.bullet_list()
|
|
|
|
content.append(notes_title)
|
|
content.append(notes)
|
|
|
|
for note in ["This document is a continuous work in progress"]:
|
|
item = nodes.list_item()
|
|
item.append(nodes.strong(text=note))
|
|
notes.append(item)
|
|
|
|
@staticmethod
|
|
def _create_cli_paragraph(feature):
|
|
"""Create a paragraph which represents the CLI commands of the feature
|
|
|
|
The paragraph will have a bullet list of CLI commands.
|
|
"""
|
|
para = nodes.paragraph()
|
|
para.append(nodes.strong(text="CLI commands:"))
|
|
commands = nodes.bullet_list()
|
|
for c in feature.cli.split(";"):
|
|
cli_command = nodes.list_item()
|
|
cli_command += nodes.literal(text=c, classes=["sp_cli"])
|
|
commands.append(cli_command)
|
|
para.append(commands)
|
|
return para
|
|
|
|
@staticmethod
|
|
def _create_notes_paragraph(notes):
|
|
"""Constructs a paragraph which represents the implementation notes
|
|
|
|
The paragraph consists of text and clickable URL nodes if links were
|
|
given in the notes.
|
|
"""
|
|
para = nodes.paragraph()
|
|
para.append(nodes.strong(text="Notes: "))
|
|
# links could start with http:// or https://
|
|
link_idxs = [m.start() for m in re.finditer('https?://', notes)]
|
|
start_idx = 0
|
|
for link_idx in link_idxs:
|
|
# assume the notes start with text (could be empty)
|
|
para.append(nodes.inline(text=notes[start_idx:link_idx]))
|
|
# create a URL node until the next text or the end of the notes
|
|
link_end_idx = notes.find(" ", link_idx)
|
|
if link_end_idx == -1:
|
|
# In case the notes end with a link without a blank
|
|
link_end_idx = len(notes)
|
|
uri = notes[link_idx:link_end_idx + 1]
|
|
para.append(nodes.reference("", uri, refuri=uri))
|
|
start_idx = link_end_idx + 1
|
|
|
|
# get all text after the last link (could be empty) or all of the
|
|
# text if no link was given
|
|
para.append(nodes.inline(text=notes[start_idx:]))
|
|
return para
|
|
|
|
|
|
def on_build_finished(app, exc):
|
|
if exc is None:
|
|
src = path.join(path.abspath(path.dirname(__file__)),
|
|
'support-matrix.css')
|
|
dst = path.join(app.outdir, '_static')
|
|
copy_asset(src, dst)
|
|
|
|
|
|
def setup(app):
|
|
app.add_directive('support_matrix', Directive)
|
|
app.add_css_file('support-matrix.css')
|
|
app.connect('build-finished', on_build_finished)
|
|
return {
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
}
|