define options with help text

Expand the way options are defined to include help text so we can
eventually include that in generated configuration files and in online
documentation.

Change-Id: I0636f5e2fb9b21519f6cdda25a1ac546a3ffe174
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2017-11-21 14:46:57 -05:00
parent 9d058ae097
commit fab39dfcc8
2 changed files with 101 additions and 48 deletions

View File

@ -10,8 +10,10 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import collections
import logging import logging
import os.path import os.path
import textwrap
import yaml import yaml
@ -20,41 +22,61 @@ from reno import defaults
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class Config(object): Opt = collections.namedtuple('Opt', 'name default help')
_OPTS = { _OPTIONS = [
# The notes subdirectory within the relnotesdir where the Opt('notesdir', defaults.NOTES_SUBDIR,
# notes live. textwrap.dedent("""
'notesdir': defaults.NOTES_SUBDIR, The notes subdirectory within the relnotesdir where the
notes live.
""")),
# Should pre-release versions be merged into the final release Opt('collapse_pre_releases', True,
# of the same number (1.0.0.0a1 notes appear under 1.0.0). textwrap.dedent("""
'collapse_pre_releases': True, Should pre-release versions be merged into the final release
of the same number (1.0.0.0a1 notes appear under 1.0.0).
""")),
# Should the scanner stop at the base of a branch (True) or go Opt('stop_at_branch_base', True,
# ahead and scan the entire history (False)? textwrap.dedent("""
'stop_at_branch_base': True, Should the scanner stop at the base of a branch (True) or go
ahead and scan the entire history (False)?
""")),
Opt('branch', None,
textwrap.dedent("""
# The git branch to scan. Defaults to the "current" branch # The git branch to scan. Defaults to the "current" branch
# checked out. # checked out.
'branch': None, """)),
Opt('earliest_version', None,
textwrap.dedent("""
# The earliest version to be included. This is usually the # The earliest version to be included. This is usually the
# lowest version number, and is meant to be the oldest # lowest version number, and is meant to be the oldest
# version. # version.
'earliest_version': None, """)),
Opt('template', defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME),
textwrap.dedent("""
# The template used by reno new to create a note. # The template used by reno new to create a note.
'template': defaults.TEMPLATE.format(defaults.PRELUDE_SECTION_NAME), """)),
Opt('release_tag_re',
textwrap.dedent('''
((?:[\d.ab]|rc)+) # digits, a, b, and rc cover regular and
# pre-releases
'''),
textwrap.dedent("""
# The RE pattern used to match the repo tags representing a valid # The RE pattern used to match the repo tags representing a valid
# release version. The pattern is compiled with the verbose and unicode # release version. The pattern is compiled with the verbose and unicode
# flags enabled. # flags enabled.
'release_tag_re': ''' """)),
((?:[\d.ab]|rc)+) # digits, a, b, and rc cover regular and
# pre-releases
''',
Opt('pre_release_tag_re',
textwrap.dedent('''
(?P<pre_release>\.\d+(?:[ab]|rc)+\d*)$
'''),
textwrap.dedent("""
# The RE pattern used to check if a valid release version tag is also a # The RE pattern used to check if a valid release version tag is also a
# valid pre-release version. The pattern is compiled with the verbose # valid pre-release version. The pattern is compiled with the verbose
# and unicode flags enabled. The pattern must define a group called # and unicode flags enabled. The pattern must define a group called
@ -62,20 +84,17 @@ class Config(object):
# separator, e.g for pre-release version '12.0.0.0rc1' the default RE # separator, e.g for pre-release version '12.0.0.0rc1' the default RE
# pattern will identify '.0rc1' as the value of the group # pattern will identify '.0rc1' as the value of the group
# 'pre_release'. # 'pre_release'.
'pre_release_tag_re': ''' """)),
(?P<pre_release>\.\d+(?:[ab]|rc)+\d*)$
''',
Opt('branch_name_re', 'stable/.+',
textwrap.dedent("""
# The pattern for names for branches that are relevant when # The pattern for names for branches that are relevant when
# 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/.+', """)),
# The identifiers and names of permitted sections in the Opt('sections',
# release notes, in the order in which the final report will [
# be generated. A prelude section will always be automatically
# inserted before the first element of this list.
'sections': [
['features', 'New Features'], ['features', 'New Features'],
['issues', 'Known Issues'], ['issues', 'Known Issues'],
['upgrade', 'Upgrade Notes'], ['upgrade', 'Upgrade Notes'],
@ -85,14 +104,24 @@ class Config(object):
['fixes', 'Bug Fixes'], ['fixes', 'Bug Fixes'],
['other', 'Other Notes'], ['other', 'Other Notes'],
], ],
textwrap.dedent("""
# The identifiers and names of permitted sections in the
# release notes, in the order in which the final report will
# be generated. A prelude section will always be automatically
# inserted before the first element of this list.
""")),
Opt('prelude_section_name', defaults.PRELUDE_SECTION_NAME,
textwrap.dedent("""
# The name of the prelude section in the note template. This # The name of the prelude section in the note template. This
# allows users to rename the section to, for example, # allows users to rename the section to, for example,
# 'release_summary' or 'project_wide_general_announcements', # 'release_summary' or 'project_wide_general_announcements',
# which is displayed in titlecase in the report after # which is displayed in titlecase in the report after
# replacing underscores with spaces. # replacing underscores with spaces.
'prelude_section_name': defaults.PRELUDE_SECTION_NAME, """)),
Opt('ignore_null_merges', True,
textwrap.dedent("""
# When this option is set to True, any merge commits with no # When this option is set to True, any merge commits with no
# changes and in which the second or later parent is tagged # changes and in which the second or later parent is tagged
# are considered "null-merges" that bring the tag information # are considered "null-merges" that bring the tag information
@ -103,20 +132,27 @@ class Config(object):
# confuses the regular traversal because it makes that stable # confuses the regular traversal because it makes that stable
# branch appear to be part of master and/or the later stable # branch appear to be part of master and/or the later stable
# branch. This option allows us to ignore those. # branch. This option allows us to ignore those.
'ignore_null_merges': True, """)),
Opt('ignore_notes', [],
textwrap.dedent("""
# Note files to be ignored. It's useful to be able to ignore a # Note files to be ignored. It's useful to be able to ignore a
# file if it is edited on the wrong branch. Notes should be # file if it is edited on the wrong branch. Notes should be
# specified by their filename or UID. Setting the value in the # specified by their filename or UID. Setting the value in the
# configuration file makes it apply to all branches. # configuration file makes it apply to all branches.
'ignore_notes': [], """)),
} ]
class Config(object):
_OPTS = {o.name: o for o in _OPTIONS}
@classmethod @classmethod
def get_default(cls, opt): def get_default(cls, opt):
"Return the default for an option." "Return the default for an option."
try: try:
return cls._OPTS[opt] return cls._OPTS[opt].default
except KeyError: except KeyError:
raise ValueError('unknown option name %r' % (opt,)) raise ValueError('unknown option name %r' % (opt,))
@ -134,7 +170,7 @@ class Config(object):
relnotesdir = defaults.RELEASE_NOTES_SUBDIR relnotesdir = defaults.RELEASE_NOTES_SUBDIR
self.relnotesdir = relnotesdir self.relnotesdir = relnotesdir
# Initialize attributes from the defaults. # Initialize attributes from the defaults.
self.override(**self._OPTS) self.override(**{o.name: o.default for o in _OPTIONS})
self._contents = {} self._contents = {}
self._load_file() self._load_file()
@ -161,7 +197,7 @@ class Config(object):
def _rename_prelude_section(self, **kwargs): def _rename_prelude_section(self, **kwargs):
key = 'prelude_section_name' key = 'prelude_section_name'
if key in kwargs and kwargs[key] != self._OPTS[key]: if key in kwargs and kwargs[key] != self._OPTS[key].default:
new_prelude_name = kwargs[key] new_prelude_name = kwargs[key]
self.template = defaults.TEMPLATE.format(new_prelude_name) self.template = defaults.TEMPLATE.format(new_prelude_name)
@ -192,9 +228,9 @@ class Config(object):
""" """
arg_values = { arg_values = {
o: getattr(parsed_args, o) o.name: getattr(parsed_args, o.name)
for o in self._OPTS.keys() for o in _OPTIONS
if hasattr(parsed_args, o) if hasattr(parsed_args, o.name)
} }
self.override(**arg_values) self.override(**arg_values)
@ -224,7 +260,10 @@ class Config(object):
Returns the actual configuration options after overrides. Returns the actual configuration options after overrides.
""" """
options = {o: getattr(self, o) for o in self._OPTS} options = {
o.name: getattr(self, o.name)
for o in _OPTIONS
}
return options return options
# def parse_config_into(parsed_arguments): # def parse_config_into(parsed_arguments):

View File

@ -37,7 +37,11 @@ collapse_pre_releases: false
def test_defaults(self): def test_defaults(self):
c = config.Config(self.tempdir.path) c = config.Config(self.tempdir.path)
actual = c.options actual = c.options
self.assertEqual(config.Config._OPTS, actual) expected = {
o.name: o.default
for o in config._OPTIONS
}
self.assertEqual(expected, actual)
def test_override(self): def test_override(self):
c = config.Config(self.tempdir.path) c = config.Config(self.tempdir.path)
@ -45,8 +49,10 @@ collapse_pre_releases: false
collapse_pre_releases=False, collapse_pre_releases=False,
) )
actual = c.options actual = c.options
expected = {} expected = {
expected.update(config.Config._OPTS) o.name: o.default
for o in config._OPTIONS
}
expected['collapse_pre_releases'] = False expected['collapse_pre_releases'] = False
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@ -59,8 +65,10 @@ collapse_pre_releases: false
notesdir='value2', notesdir='value2',
) )
actual = c.options actual = c.options
expected = {} expected = {
expected.update(config.Config._OPTS) o.name: o.default
for o in config._OPTIONS
}
expected['notesdir'] = 'value2' expected['notesdir'] = 'value2'
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
@ -108,18 +116,24 @@ collapse_pre_releases: false
def test_override_from_parsed_args_empty(self): def test_override_from_parsed_args_empty(self):
c = self._run_override_from_parsed_args([]) c = self._run_override_from_parsed_args([])
actual = { actual = {
o: getattr(c, o) o.name: getattr(c, o.name)
for o in config.Config._OPTS.keys() for o in config._OPTIONS
} }
self.assertEqual(config.Config._OPTS, actual) expected = {
o.name: o.default
for o in config._OPTIONS
}
self.assertEqual(expected, actual)
def test_override_from_parsed_args(self): def test_override_from_parsed_args(self):
c = self._run_override_from_parsed_args([ c = self._run_override_from_parsed_args([
'--no-collapse-pre-releases', '--no-collapse-pre-releases',
]) ])
actual = c.options actual = c.options
expected = {} expected = {
expected.update(config.Config._OPTS) o.name: o.default
for o in config._OPTIONS
}
expected['collapse_pre_releases'] = False expected['collapse_pre_releases'] = False
self.assertEqual(expected, actual) self.assertEqual(expected, actual)