releases/openstack_releases/sphinxext.py
Doug Hellmann cdd514b9f4 collapse pre-releases into final releases
When a final release is prepared, stop showing any preceding
pre-releases for that same version.

Change-Id: I0a73c72de1af5796f90deaaabbc2dbaba7abee32
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
2016-02-26 13:51:06 -05:00

286 lines
9.5 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 _tarball_link(version, project):
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,
)
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('')
_list_table(
_add,
['Version', 'Repo', 'Git Commit'],
((self._tarball_link(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)