Refactor Section config to use NamedTuple
Users configure the sections as a List[List[str]]. That continues to be the case. But now we parse that raw list into a list of a NamedTuple called Section. This makes our consuming code, `formatter.py` and `linter.py` more clear because we can access the values by name, rather than index. But more importantly, this is a "prefactor" to add support for nested headers. In a followup, we will add the field 'top_level: bool' so that `formatter.py` can know how to render the section. This factoring simplifies that change. Change-Id: Ie80a525af61e879dd872079b2b9d0513db40e82d
This commit is contained in:
parent
04233e0eae
commit
919210c386
@ -10,10 +10,12 @@
|
|||||||
# 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 textwrap
|
||||||
|
from typing import Any
|
||||||
|
from typing import List
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
@ -22,7 +24,20 @@ from reno import defaults
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
Opt = collections.namedtuple('Opt', 'name default help')
|
class Opt(NamedTuple):
|
||||||
|
name: str
|
||||||
|
default: Any
|
||||||
|
help: str
|
||||||
|
|
||||||
|
|
||||||
|
class Section(NamedTuple):
|
||||||
|
name: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_raw_yaml(cls, val: List[List[str]]) -> List["Section"]:
|
||||||
|
return [Section(*entry) for entry in val]
|
||||||
|
|
||||||
|
|
||||||
_OPTIONS = [
|
_OPTIONS = [
|
||||||
Opt('notesdir', defaults.NOTES_SUBDIR,
|
Opt('notesdir', defaults.NOTES_SUBDIR,
|
||||||
@ -219,13 +234,13 @@ _OPTIONS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config:
|
||||||
|
|
||||||
_OPTS = {o.name: o for o in _OPTIONS}
|
_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].default
|
return cls._OPTS[opt].default
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -301,12 +316,14 @@ class Config(object):
|
|||||||
# Replace prelude section name if it has been changed.
|
# Replace prelude section name if it has been changed.
|
||||||
self._rename_prelude_section(**kwds)
|
self._rename_prelude_section(**kwds)
|
||||||
|
|
||||||
for n, v in kwds.items():
|
for name, val in kwds.items():
|
||||||
if n not in self._OPTS:
|
if name not in self._OPTS:
|
||||||
LOG.warning('ignoring unknown configuration value %r = %r',
|
LOG.warning('ignoring unknown configuration value %r = %r',
|
||||||
n, v)
|
name, val)
|
||||||
else:
|
else:
|
||||||
setattr(self, n, v)
|
if name == "sections":
|
||||||
|
val = Section.from_raw_yaml(val)
|
||||||
|
setattr(self, name, val)
|
||||||
|
|
||||||
def override_from_parsed_args(self, parsed_args):
|
def override_from_parsed_args(self, parsed_args):
|
||||||
"""Set the values of the configuration options from parsed CLI args.
|
"""Set the values of the configuration options from parsed CLI args.
|
||||||
|
@ -94,19 +94,19 @@ def format_report(loader, config, versions_to_include, title=None,
|
|||||||
report.append('')
|
report.append('')
|
||||||
|
|
||||||
# Add other sections.
|
# Add other sections.
|
||||||
for section_name, section_title in config.sections:
|
for section in config.sections:
|
||||||
notes = [
|
notes = [
|
||||||
(n, fn, sha)
|
(n, fn, sha)
|
||||||
for fn, sha in notefiles
|
for fn, sha in notefiles
|
||||||
if file_contents[fn].get(section_name)
|
if file_contents[fn].get(section.name)
|
||||||
for n in file_contents[fn].get(section_name, [])
|
for n in file_contents[fn].get(section.name, [])
|
||||||
]
|
]
|
||||||
if notes:
|
if notes:
|
||||||
report.append(_section_anchor(
|
report.append(_section_anchor(
|
||||||
section_title, version_title, title, branch))
|
section.title, version_title, title, branch))
|
||||||
report.append('')
|
report.append('')
|
||||||
report.append(section_title)
|
report.append(section.title)
|
||||||
report.append('-' * len(section_title))
|
report.append('-' * len(section.title))
|
||||||
report.append('')
|
report.append('')
|
||||||
for n, fn, sha in notes:
|
for n, fn, sha in notes:
|
||||||
if show_source:
|
if show_source:
|
||||||
|
@ -21,14 +21,14 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def lint_cmd(args, conf):
|
def lint_cmd(args, conf):
|
||||||
"Check some common mistakes"
|
"""Check some common mistakes"""
|
||||||
LOG.debug('starting lint')
|
LOG.debug('starting lint')
|
||||||
notesdir = os.path.join(conf.reporoot, conf.notespath)
|
notesdir = os.path.join(conf.reporoot, conf.notespath)
|
||||||
notes = glob.glob(os.path.join(notesdir, '*.yaml'))
|
notes = glob.glob(os.path.join(notesdir, '*.yaml'))
|
||||||
|
|
||||||
error = 0
|
error = 0
|
||||||
allowed_section_names = [conf.prelude_section_name] + \
|
allowed_section_names = [conf.prelude_section_name] + \
|
||||||
[s[0] for s in conf.sections]
|
[s.name for s in conf.sections]
|
||||||
|
|
||||||
uids = {}
|
uids = {}
|
||||||
with loader.Loader(conf, ignore_cache=True) as ldr:
|
with loader.Loader(conf, ignore_cache=True) as ldr:
|
||||||
|
@ -143,6 +143,9 @@ class Loader(object):
|
|||||||
f'mapping. Did you forget a top-level key?'
|
f'mapping. Did you forget a top-level key?'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
valid_section_names = {
|
||||||
|
section.name for section in self._config.sections
|
||||||
|
}
|
||||||
for section_name, section_content in content.items():
|
for section_name, section_content in content.items():
|
||||||
if section_name == self._config.prelude_section_name:
|
if section_name == self._config.prelude_section_name:
|
||||||
if not isinstance(section_content, str):
|
if not isinstance(section_content, str):
|
||||||
@ -152,14 +155,14 @@ class Loader(object):
|
|||||||
section_name, filename,
|
section_name, filename,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if section_name not in dict(self._config.sections):
|
if section_name not in valid_section_names:
|
||||||
# TODO(stephenfin): Make this an error in a future release
|
# TODO(stephenfin): Make this an error in a future release
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
'The %s section of %s is not a recognized section. '
|
'The %s section of %s is not a recognized section. '
|
||||||
'It should be one of: %s. '
|
'It should be one of: %s. '
|
||||||
'This will be an error in a future release.',
|
'This will be an error in a future release.',
|
||||||
section_name, filename,
|
section_name, filename,
|
||||||
', '.join(dict(self._config.sections)),
|
', '.join(valid_section_names),
|
||||||
)
|
)
|
||||||
if isinstance(section_content, str):
|
if isinstance(section_content, str):
|
||||||
# A single string is OK, but wrap it with a list
|
# A single string is OK, but wrap it with a list
|
||||||
|
@ -18,11 +18,26 @@ from unittest import mock
|
|||||||
import fixtures
|
import fixtures
|
||||||
|
|
||||||
from reno import config
|
from reno import config
|
||||||
|
from reno.config import Section
|
||||||
from reno import defaults
|
from reno import defaults
|
||||||
from reno import main
|
from reno import main
|
||||||
from reno.tests import base
|
from reno.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
def expected_options(**overrides):
|
||||||
|
"""The default config options, along with any overrides set via kwargs."""
|
||||||
|
result = {
|
||||||
|
o.name: (
|
||||||
|
Section.from_raw_yaml(o.default)
|
||||||
|
if o.name == "sections"
|
||||||
|
else o.default
|
||||||
|
)
|
||||||
|
for o in config._OPTIONS
|
||||||
|
}
|
||||||
|
result.update(**overrides)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class TestConfig(base.TestCase):
|
class TestConfig(base.TestCase):
|
||||||
EXAMPLE_CONFIG = """
|
EXAMPLE_CONFIG = """
|
||||||
collapse_pre_releases: false
|
collapse_pre_releases: false
|
||||||
@ -36,11 +51,7 @@ 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
|
||||||
expected = {
|
self.assertEqual(expected_options(), actual)
|
||||||
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)
|
||||||
@ -48,11 +59,7 @@ collapse_pre_releases: false
|
|||||||
collapse_pre_releases=False,
|
collapse_pre_releases=False,
|
||||||
)
|
)
|
||||||
actual = c.options
|
actual = c.options
|
||||||
expected = {
|
expected = expected_options(collapse_pre_releases=False)
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
expected['collapse_pre_releases'] = False
|
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_override_multiple(self):
|
def test_override_multiple(self):
|
||||||
@ -64,11 +71,7 @@ collapse_pre_releases: false
|
|||||||
notesdir='value2',
|
notesdir='value2',
|
||||||
)
|
)
|
||||||
actual = c.options
|
actual = c.options
|
||||||
expected = {
|
expected = expected_options(notesdir='value2')
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
expected['notesdir'] = 'value2'
|
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_load_file_not_present(self):
|
def test_load_file_not_present(self):
|
||||||
@ -127,22 +130,14 @@ collapse_pre_releases: false
|
|||||||
o.name: getattr(c, o.name)
|
o.name: getattr(c, o.name)
|
||||||
for o in config._OPTIONS
|
for o in config._OPTIONS
|
||||||
}
|
}
|
||||||
expected = {
|
self.assertEqual(expected_options(), actual)
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
self.assertEqual(expected, actual)
|
|
||||||
|
|
||||||
def test_override_from_parsed_args_boolean_false(self):
|
def test_override_from_parsed_args_boolean_false(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_options(collapse_pre_releases=False)
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
expected['collapse_pre_releases'] = False
|
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_override_from_parsed_args_boolean_true(self):
|
def test_override_from_parsed_args_boolean_true(self):
|
||||||
@ -150,11 +145,7 @@ collapse_pre_releases: false
|
|||||||
'--collapse-pre-releases',
|
'--collapse-pre-releases',
|
||||||
])
|
])
|
||||||
actual = c.options
|
actual = c.options
|
||||||
expected = {
|
expected = expected_options(collapse_pre_releases=True)
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
expected['collapse_pre_releases'] = True
|
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_override_from_parsed_args_string(self):
|
def test_override_from_parsed_args_string(self):
|
||||||
@ -162,11 +153,7 @@ collapse_pre_releases: false
|
|||||||
'--earliest-version', '1.2.3',
|
'--earliest-version', '1.2.3',
|
||||||
])
|
])
|
||||||
actual = c.options
|
actual = c.options
|
||||||
expected = {
|
expected = expected_options(earliest_version='1.2.3')
|
||||||
o.name: o.default
|
|
||||||
for o in config._OPTIONS
|
|
||||||
}
|
|
||||||
expected['earliest_version'] = '1.2.3'
|
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
def test_override_from_parsed_args_ignore_non_options(self):
|
def test_override_from_parsed_args_ignore_non_options(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user