make sections configurable
The list of section identifiers and corresponding display names, and the order in which they are rendered, was hard-coded. Some projects want to customise this, so move it into the Config object so that it can now be specified via config.yaml, e.g. sections: - [features, New Features] - [issues, Known Issues] - [upgrade, Upgrade Notes] - [api, API Changes] - [security, Security Issues] - [fixes, Bug Fixes] Change-Id: I914572c6a07ca81c54965b4b5a6b6aba50b3a787
This commit is contained in:
parent
10ccdda0eb
commit
081a4145e1
@ -50,13 +50,16 @@ Editing a Release Note
|
|||||||
======================
|
======================
|
||||||
|
|
||||||
The note file is a YAML file with several sections. All of the text is
|
The note file is a YAML file with several sections. All of the text is
|
||||||
interpreted as having `reStructuredText`_ formatting.
|
interpreted as having `reStructuredText`_ formatting. The permitted
|
||||||
|
sections are configurable (see below) but default to the following
|
||||||
|
list:
|
||||||
|
|
||||||
prelude
|
prelude
|
||||||
|
|
||||||
General comments about the release. The prelude from all notes in a
|
General comments about the release. The prelude from all notes in a
|
||||||
section are combined, in note order, to produce a single prelude
|
section are combined, in note order, to produce a single prelude
|
||||||
introducing that release.
|
introducing that release. This section is always included, regardless
|
||||||
|
of what sections are configured.
|
||||||
|
|
||||||
features
|
features
|
||||||
|
|
||||||
@ -177,6 +180,14 @@ may be the most convenient way to manage the values consistently.
|
|||||||
earliest_version: 12.0.0
|
earliest_version: 12.0.0
|
||||||
collapse_pre_releases: false
|
collapse_pre_releases: false
|
||||||
stop_at_branch_base: true
|
stop_at_branch_base: true
|
||||||
|
sections:
|
||||||
|
# The prelude section is implicitly included.
|
||||||
|
- [features, New Features]
|
||||||
|
- [issues, Known Issues]
|
||||||
|
- [upgrade, Upgrade Notes]
|
||||||
|
- [api, API Changes]
|
||||||
|
- [security, Security Issues]
|
||||||
|
- [fixes, Bug Fixes]
|
||||||
template: |
|
template: |
|
||||||
<template-used-to-create-new-notes>
|
<template-used-to-create-new-notes>
|
||||||
...
|
...
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add a configuration option ``sections`` to hold the list of
|
||||||
|
permitted section identifiers and corresponding display names.
|
||||||
|
This also determines the order in which sections are collated.
|
||||||
|
|
@ -140,6 +140,20 @@ class Config(object):
|
|||||||
# scanning history to determine where to stop, to find the
|
# scanning history to determine where to stop, to find the
|
||||||
# "base" of a branch. Other branches are ignored.
|
# "base" of a branch. Other branches are ignored.
|
||||||
'branch_name_re': 'stable/.+',
|
'branch_name_re': 'stable/.+',
|
||||||
|
|
||||||
|
# The identifiers and names of permitted sections in the
|
||||||
|
# release notes, in the order in which the final report will
|
||||||
|
# be generated.
|
||||||
|
'sections': [
|
||||||
|
['features', 'New Features'],
|
||||||
|
['issues', 'Known Issues'],
|
||||||
|
['upgrade', 'Upgrade Notes'],
|
||||||
|
['deprecations', 'Deprecation Notes'],
|
||||||
|
['critical', 'Critical Issues'],
|
||||||
|
['security', 'Security Issues'],
|
||||||
|
['fixes', 'Bug Fixes'],
|
||||||
|
['other', 'Other Notes'],
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -13,18 +13,6 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
|
||||||
_SECTION_ORDER = [
|
|
||||||
('features', 'New Features'),
|
|
||||||
('issues', 'Known Issues'),
|
|
||||||
('upgrade', 'Upgrade Notes'),
|
|
||||||
('deprecations', 'Deprecation Notes'),
|
|
||||||
('critical', 'Critical Issues'),
|
|
||||||
('security', 'Security Issues'),
|
|
||||||
('fixes', 'Bug Fixes'),
|
|
||||||
('other', 'Other Notes'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _indent_for_list(text, prefix=' '):
|
def _indent_for_list(text, prefix=' '):
|
||||||
"""Indent some text to make it work as a list entry.
|
"""Indent some text to make it work as a list entry.
|
||||||
|
|
||||||
@ -37,7 +25,7 @@ def _indent_for_list(text, prefix=' '):
|
|||||||
]) + '\n'
|
]) + '\n'
|
||||||
|
|
||||||
|
|
||||||
def format_report(loader, versions_to_include, title=None):
|
def format_report(loader, config, versions_to_include, title=None):
|
||||||
report = []
|
report = []
|
||||||
if title:
|
if title:
|
||||||
report.append('=' * len(title))
|
report.append('=' * len(title))
|
||||||
@ -64,7 +52,7 @@ def format_report(loader, versions_to_include, title=None):
|
|||||||
report.append(file_contents[n]['prelude'])
|
report.append(file_contents[n]['prelude'])
|
||||||
report.append('')
|
report.append('')
|
||||||
|
|
||||||
for section_name, section_title in _SECTION_ORDER:
|
for section_name, section_title in config.sections:
|
||||||
notes = [
|
notes = [
|
||||||
n
|
n
|
||||||
for fn, sha in notefiles
|
for fn, sha in notefiles
|
||||||
|
@ -25,6 +25,7 @@ def report_cmd(args, conf):
|
|||||||
versions = ldr.versions
|
versions = ldr.versions
|
||||||
text = formatter.format_report(
|
text = formatter.format_report(
|
||||||
ldr,
|
ldr,
|
||||||
|
conf,
|
||||||
versions,
|
versions,
|
||||||
title='Release Notes',
|
title='Release Notes',
|
||||||
)
|
)
|
||||||
|
@ -90,6 +90,7 @@ class ReleaseNotesDirective(rst.Directive):
|
|||||||
info('got versions %s' % (versions,))
|
info('got versions %s' % (versions,))
|
||||||
text = formatter.format_report(
|
text = formatter.format_report(
|
||||||
ldr,
|
ldr,
|
||||||
|
conf,
|
||||||
versions,
|
versions,
|
||||||
title=title,
|
title=title,
|
||||||
)
|
)
|
||||||
|
@ -20,7 +20,7 @@ from reno import loader
|
|||||||
from reno.tests import base
|
from reno.tests import base
|
||||||
|
|
||||||
|
|
||||||
class TestFormatter(base.TestCase):
|
class TestFormatterBase(base.TestCase):
|
||||||
|
|
||||||
scanner_output = {
|
scanner_output = {
|
||||||
'0.0.0': [('note1', 'shaA')],
|
'0.0.0': [('note1', 'shaA')],
|
||||||
@ -29,6 +29,29 @@ class TestFormatter(base.TestCase):
|
|||||||
|
|
||||||
versions = ['0.0.0', '1.0.0']
|
versions = ['0.0.0', '1.0.0']
|
||||||
|
|
||||||
|
def _get_note_body(self, reporoot, filename, sha):
|
||||||
|
return self.note_bodies.get(filename, '')
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestFormatterBase, self).setUp()
|
||||||
|
|
||||||
|
def _load(ldr):
|
||||||
|
ldr._scanner_output = self.scanner_output
|
||||||
|
ldr._cache = {
|
||||||
|
'file-contents': self.note_bodies
|
||||||
|
}
|
||||||
|
|
||||||
|
self.c = config.Config('reporoot')
|
||||||
|
|
||||||
|
with mock.patch('reno.loader.Loader._load_data', _load):
|
||||||
|
self.ldr = loader.Loader(
|
||||||
|
self.c,
|
||||||
|
ignore_cache=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatter(TestFormatterBase):
|
||||||
|
|
||||||
note_bodies = {
|
note_bodies = {
|
||||||
'note1': {
|
'note1': {
|
||||||
'prelude': 'This is the prelude.',
|
'prelude': 'This is the prelude.',
|
||||||
@ -47,29 +70,10 @@ class TestFormatter(base.TestCase):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_note_body(self, reporoot, filename, sha):
|
|
||||||
return self.note_bodies.get(filename, '')
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(TestFormatter, self).setUp()
|
|
||||||
|
|
||||||
def _load(ldr):
|
|
||||||
ldr._scanner_output = self.scanner_output
|
|
||||||
ldr._cache = {
|
|
||||||
'file-contents': self.note_bodies
|
|
||||||
}
|
|
||||||
|
|
||||||
self.c = config.Config('reporoot')
|
|
||||||
|
|
||||||
with mock.patch('reno.loader.Loader._load_data', _load):
|
|
||||||
self.ldr = loader.Loader(
|
|
||||||
self.c,
|
|
||||||
ignore_cache=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_with_title(self):
|
def test_with_title(self):
|
||||||
result = formatter.format_report(
|
result = formatter.format_report(
|
||||||
loader=self.ldr,
|
loader=self.ldr,
|
||||||
|
config=self.c,
|
||||||
versions_to_include=self.versions,
|
versions_to_include=self.versions,
|
||||||
title='This is the title',
|
title='This is the title',
|
||||||
)
|
)
|
||||||
@ -78,6 +82,7 @@ class TestFormatter(base.TestCase):
|
|||||||
def test_versions(self):
|
def test_versions(self):
|
||||||
result = formatter.format_report(
|
result = formatter.format_report(
|
||||||
loader=self.ldr,
|
loader=self.ldr,
|
||||||
|
config=self.c,
|
||||||
versions_to_include=self.versions,
|
versions_to_include=self.versions,
|
||||||
title='This is the title',
|
title='This is the title',
|
||||||
)
|
)
|
||||||
@ -87,14 +92,16 @@ class TestFormatter(base.TestCase):
|
|||||||
def test_without_title(self):
|
def test_without_title(self):
|
||||||
result = formatter.format_report(
|
result = formatter.format_report(
|
||||||
loader=self.ldr,
|
loader=self.ldr,
|
||||||
|
config=self.c,
|
||||||
versions_to_include=self.versions,
|
versions_to_include=self.versions,
|
||||||
title=None,
|
title=None,
|
||||||
)
|
)
|
||||||
self.assertNotIn('This is the title', result)
|
self.assertNotIn('This is the title', result)
|
||||||
|
|
||||||
def test_section_order(self):
|
def test_default_section_order(self):
|
||||||
result = formatter.format_report(
|
result = formatter.format_report(
|
||||||
loader=self.ldr,
|
loader=self.ldr,
|
||||||
|
config=self.c,
|
||||||
versions_to_include=self.versions,
|
versions_to_include=self.versions,
|
||||||
title=None,
|
title=None,
|
||||||
)
|
)
|
||||||
@ -104,3 +111,48 @@ class TestFormatter(base.TestCase):
|
|||||||
expected = [prelude_pos, features_pos, issues_pos]
|
expected = [prelude_pos, features_pos, issues_pos]
|
||||||
actual = list(sorted([prelude_pos, features_pos, issues_pos]))
|
actual = list(sorted([prelude_pos, features_pos, issues_pos]))
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatterCustomSections(TestFormatterBase):
|
||||||
|
note_bodies = {
|
||||||
|
'note1': {
|
||||||
|
'prelude': 'This is the prelude.',
|
||||||
|
},
|
||||||
|
'note2': {
|
||||||
|
'features': [
|
||||||
|
'This is the first feature.',
|
||||||
|
],
|
||||||
|
'api': [
|
||||||
|
'This is the API change for the first feature.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'note3': {
|
||||||
|
'api': [
|
||||||
|
'This is the API change for the second feature.',
|
||||||
|
],
|
||||||
|
'features': [
|
||||||
|
'This is the second feature.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestFormatterCustomSections, self).setUp()
|
||||||
|
self.c.override(sections=[
|
||||||
|
['api', 'API Changes'],
|
||||||
|
['features', 'New Features'],
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_custom_section_order(self):
|
||||||
|
result = formatter.format_report(
|
||||||
|
loader=self.ldr,
|
||||||
|
config=self.c,
|
||||||
|
versions_to_include=self.versions,
|
||||||
|
title=None,
|
||||||
|
)
|
||||||
|
prelude_pos = result.index('This is the prelude.')
|
||||||
|
api_pos = result.index('API Changes')
|
||||||
|
features_pos = result.index('New Features')
|
||||||
|
expected = [prelude_pos, api_pos, features_pos]
|
||||||
|
actual = list(sorted([prelude_pos, features_pos, api_pos]))
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
Loading…
Reference in New Issue
Block a user