releases/openstack_releases/sphinxext.py
Doug Hellmann bf2443d340 add artifact-link-mode option
The openstack-ansible project doesn't produce separate tarball
artifacts, so give them a way to turn off links that point to files that
don't exist.

Change-Id: I3fa5a053b61cd57e65222cde785ea7742870c8ee
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2016-03-11 13:47:24 -05:00

293 lines
9.9 KiB
Python

# All Rights Reserved.
#
# 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.
import collections
import glob
import os.path
from docutils import nodes
from docutils.parsers import rst
from docutils.parsers.rst import directives
from docutils.statemachine import ViewList
import pbr
from sphinx.util.nodes import nested_parse_with_titles
import yaml
from openstack_releases import governance
def _list_table(add, headers, data, title='', columns=None):
"""Build a list-table directive.
:param add: Function to add one row to output.
:param headers: List of header values.
:param data: Iterable of row data, yielding lists or tuples with rows.
"""
add('.. list-table:: %s' % title)
add(' :header-rows: 1')
if columns:
add(' :widths: %s' % (','.join(str(c) for c in columns)))
add('')
add(' - * %s' % headers[0])
for h in headers[1:]:
add(' * %s' % h)
for row in data:
add(' - * %s' % row[0])
for r in row[1:]:
add(' * %s' % r)
add('')
def _get_deliverable_type(deliverable_types, name):
if (name.startswith('python-') and not name.endswith('client')):
name = name[7:]
if (name.startswith('python-') and name.endswith('client')):
return 'type:library'
if name in deliverable_types:
return deliverable_types[name]
no_dashes = name.replace('-', '_')
if no_dashes in deliverable_types:
return deliverable_types[no_dashes]
return 'type:other'
def _collapse_deliverable_history(app, name, info):
"""Collapse pre-releases into their final release.
Edit the info dictionary in place.
"""
# Collapse pre-releases into their final release.
releases = []
known_versions = set()
for r in reversed(info.get('releases', [])):
try:
parsed_vers = pbr.version.SemanticVersion.from_pip_string(
str(r['version']))
vers_tuple = parsed_vers.version_tuple()
except:
# If we can't parse the version, it must be some sort
# of made up legacy tag. Ignore the parse error
# and include the value in our output.
releases.append(r)
else:
if len(vers_tuple) != 3:
# This is not a normal release, so assume it
# is a pre-release.
final = parsed_vers.brief_string()
if final in known_versions:
app.info('[deliverables] ignoring %s %s' %
(name, r['version']))
continue
releases.append(r)
known_versions.add(r['version'])
info['releases'] = list(reversed(releases))
class DeliverableDirectiveBase(rst.Directive):
option_spec = {
'series': directives.unchanged,
}
_TYPE_ORDER = [
'type:service',
'type:library',
'type:other',
]
def run(self):
env = self.state.document.settings.env
app = env.app
team_data = governance.get_team_data()
# The series value is optional for some directives.
series = self.options.get('series')
deliverable_types = {}
for team in (governance.Team(n, i) for n, i in team_data.items()):
for dn, di in team.deliverables.items():
for tag in di.tags:
if tag.startswith('type:'):
deliverable_types[dn] = tag
result = ViewList()
# Read all of the deliverable data for the series.
deliverables = collections.defaultdict(list)
for filename in sorted(self._get_deliverables_files(series)):
app.info('[deliverables] reading %s' % filename)
deliverable_name = os.path.basename(filename)[:-5] # strip .yaml
deliverable_type = _get_deliverable_type(
deliverable_types,
deliverable_name,
)
with open(filename, 'r') as f:
d_info = yaml.load(f.read())
_collapse_deliverable_history(app, deliverable_name, d_info)
deliverables[deliverable_type].append(
(deliverable_name,
filename,
d_info))
for type_tag in self._TYPE_ORDER:
self._add_deliverables(
type_tag,
deliverables[type_tag],
series,
app,
result,
)
# NOTE(dhellmann): Useful for debugging.
# print('\n'.join(result))
node = nodes.section()
node.document = self.state.document
nested_parse_with_titles(self.state, result, node)
return node.children
_TYPE_TITLE = {
'type:service': 'Service Projects',
'type:library': 'Library Projects',
'type:other': 'Other Projects',
}
@staticmethod
def _artifact_link(mode, version, project):
if mode == 'tarball':
# Link the version number to the tarball for downloading.
repo_base = project['repo'].rsplit('/')[-1]
if 'tarball-base' in project:
base = project['tarball-base']
else:
base = repo_base
return '`{v} <{s}/{r}/{n}-{v}.tar.gz>`__'.format(
s='https://tarballs.openstack.org',
v=version,
r=repo_base,
n=base,
)
elif mode == 'none':
# Only show the version number.
return version
raise ValueError('Unrecognized artifact-link-mode: %r' % mode)
def _add_deliverables(self, type_tag, deliverables, series, app, result):
source_name = '<' + __name__ + '>'
if not deliverables:
# There are no deliverables of this type, and that's OK.
return
result.append('', source_name)
title = self._TYPE_TITLE.get(type_tag, 'Unknown Projects')
result.append('-' * len(title), source_name)
result.append(title, source_name)
result.append('-' * len(title), source_name)
result.append('', source_name)
# Build a table of the first and most recent versions of each
# deliverable.
most_recent = []
for deliverable_name, filename, deliverable_info in deliverables:
earliest_version = deliverable_info.get('releases', {})[0].get(
'version', 'unreleased')
recent_version = deliverable_info.get('releases', {})[-1].get(
'version', 'unreleased')
ref = ':ref:`%s-%s`' % (series, deliverable_name)
release_notes = deliverable_info.get('release-notes')
if release_notes:
notes_link = '`release notes <%s>`__' % release_notes
else:
notes_link = ''
most_recent.append((ref, earliest_version, recent_version, notes_link))
_list_table(
lambda t: result.append(t, source_name),
['Deliverable', 'Earliest Version', 'Most Recent Version', 'Notes'],
most_recent,
title='Release Summary',
)
# Show the detailed history of the deliverables within the series.
for deliverable_name, filename, deliverable_info in deliverables:
# These closures need to be redefined in each iteration of
# the loop because they use the filename.
def _add(text):
result.append(text, filename)
def _title(text, underline):
text = str(text) # version numbers might be seen as floats
_add('.. _%s-%s:' % (series, text))
_add('')
_add(text)
_add(underline * len(text))
_add('')
_title(deliverable_name, '=')
app.info('[deliverables] rendering %s' % deliverable_name)
release_notes = deliverable_info.get('release-notes')
if release_notes:
_add('')
_add('Release Notes: %s' % release_notes)
_add('')
link_mode = deliverable_info.get('artifact-link-mode', 'tarball')
_list_table(
_add,
['Version', 'Repo', 'Git Commit'],
((self._artifact_link(link_mode, r['version'], p),
p['repo'], p['hash'])
for r in reversed(deliverable_info.get('releases', []))
for p in r.get('projects', [])),
columns=[10, 40, 50],
)
class DeliverableDirective(DeliverableDirectiveBase):
def _get_deliverables_files(self, series):
return glob.glob('deliverables/%s/*.yaml' % series)
def run(self):
# Require a series value.
series = self.options.get('series')
if not series:
error = self.state_machine.reporter.error(
'No series set for deliverable directive',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [error]
return super(DeliverableDirective, self).run()
class IndependentDeliverablesDirective(DeliverableDirectiveBase):
def _get_deliverables_files(self, series):
return glob.glob('deliverables/_independent/*.yaml')
def setup(app):
app.add_directive('deliverable', DeliverableDirective)
app.add_directive('independent-deliverables',
IndependentDeliverablesDirective)