diff --git a/doc/source/usage.rst b/doc/source/usage.rst index e21da98..53fb7a3 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -50,13 +50,16 @@ Editing a Release Note ====================== 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 General comments about the release. The prelude from all notes in a 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 @@ -177,6 +180,14 @@ may be the most convenient way to manage the values consistently. earliest_version: 12.0.0 collapse_pre_releases: false 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: | ... diff --git a/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml b/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml new file mode 100644 index 0000000..73c77bc --- /dev/null +++ b/releasenotes/notes/config-option-sections-9c68b070698e984a.yaml @@ -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. + diff --git a/reno/config.py b/reno/config.py index 47d44b1..2dd6105 100644 --- a/reno/config.py +++ b/reno/config.py @@ -140,6 +140,20 @@ class Config(object): # scanning history to determine where to stop, to find the # "base" of a branch. Other branches are ignored. '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 diff --git a/reno/formatter.py b/reno/formatter.py index 749dfbe..aebe6f2 100644 --- a/reno/formatter.py +++ b/reno/formatter.py @@ -13,18 +13,6 @@ 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=' '): """Indent some text to make it work as a list entry. @@ -37,7 +25,7 @@ def _indent_for_list(text, prefix=' '): ]) + '\n' -def format_report(loader, versions_to_include, title=None): +def format_report(loader, config, versions_to_include, title=None): report = [] if 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('') - for section_name, section_title in _SECTION_ORDER: + for section_name, section_title in config.sections: notes = [ n for fn, sha in notefiles diff --git a/reno/report.py b/reno/report.py index e59520f..2a59410 100644 --- a/reno/report.py +++ b/reno/report.py @@ -25,6 +25,7 @@ def report_cmd(args, conf): versions = ldr.versions text = formatter.format_report( ldr, + conf, versions, title='Release Notes', ) diff --git a/reno/sphinxext.py b/reno/sphinxext.py index 85b1a2a..3079f70 100644 --- a/reno/sphinxext.py +++ b/reno/sphinxext.py @@ -90,6 +90,7 @@ class ReleaseNotesDirective(rst.Directive): info('got versions %s' % (versions,)) text = formatter.format_report( ldr, + conf, versions, title=title, ) diff --git a/reno/tests/test_formatter.py b/reno/tests/test_formatter.py index 42dc52c..23ed3e1 100644 --- a/reno/tests/test_formatter.py +++ b/reno/tests/test_formatter.py @@ -20,7 +20,7 @@ from reno import loader from reno.tests import base -class TestFormatter(base.TestCase): +class TestFormatterBase(base.TestCase): scanner_output = { '0.0.0': [('note1', 'shaA')], @@ -29,6 +29,29 @@ class TestFormatter(base.TestCase): 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 = { 'note1': { '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): result = formatter.format_report( loader=self.ldr, + config=self.c, versions_to_include=self.versions, title='This is the title', ) @@ -78,6 +82,7 @@ class TestFormatter(base.TestCase): def test_versions(self): result = formatter.format_report( loader=self.ldr, + config=self.c, versions_to_include=self.versions, title='This is the title', ) @@ -87,14 +92,16 @@ class TestFormatter(base.TestCase): def test_without_title(self): result = formatter.format_report( loader=self.ldr, + config=self.c, versions_to_include=self.versions, title=None, ) self.assertNotIn('This is the title', result) - def test_section_order(self): + def test_default_section_order(self): result = formatter.format_report( loader=self.ldr, + config=self.c, versions_to_include=self.versions, title=None, ) @@ -104,3 +111,48 @@ class TestFormatter(base.TestCase): expected = [prelude_pos, features_pos, issues_pos] actual = list(sorted([prelude_pos, features_pos, issues_pos])) 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)