Add YAML configuration parsing
While discussing I7539fdeada14a73ae4e18a125bb0e3947f08e8d1 Doug and Harry realized that it would be much better if some of these values could be specified in a config file, whether INI or YAML. This adds a simple function to allow options to be specified in the config file named "config.yml" in the --rel-notes-dir. Change-Id: Ie25e1eb3da66cc627d93af585b0893469d6a7b2e
This commit is contained in:
parent
3b9cf44feb
commit
750bdc021b
|
@ -147,3 +147,28 @@ repeated).
|
|||
Notes are output in the order they are found by ``git log`` looking
|
||||
over the history of the branch. This is deterministic, but not
|
||||
necessarily predictable or mutable.
|
||||
|
||||
Configuring Reno
|
||||
================
|
||||
|
||||
Reno looks for an optional ``config.yml`` file in your release notes
|
||||
directory. This file may contain optional flags that you might use with a
|
||||
command. If the values do not apply to the command, they are ignored in the
|
||||
configuration file. For example, a couple reno commands allow you to specify
|
||||
|
||||
- ``--branch``
|
||||
- ``--earliest-version``
|
||||
- ``--collapse-pre-releases``/``--no-collapse-pre-releases``
|
||||
- ``--ignore-cache``
|
||||
|
||||
So you might write a config file (if you use these often) like:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
---
|
||||
branch: master
|
||||
earliest_version: 12.0.0
|
||||
collapse_pre_releases: false
|
||||
|
||||
These will be parsed first and then the CLI options will be applied after
|
||||
the config files.
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
prelude: >
|
||||
Reno now supports having a configuration file!
|
||||
features:
|
||||
- |
|
||||
Reno now supports having a ``config.yaml`` file in your release notes
|
||||
directory. It will search for file in the directory specified by
|
||||
``--rel-notes-dir`` and parse it. It will apply whatever options are
|
||||
valid for that particular command. If an option is not relevant to a
|
||||
particular sub-command, it will not attempt to apply them.
|
|
@ -0,0 +1,73 @@
|
|||
# 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 logging
|
||||
import os.path
|
||||
|
||||
import yaml
|
||||
|
||||
from reno import defaults
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config_path(relnotesdir):
|
||||
"""Generate the path to the config file.
|
||||
|
||||
:param str relnotesdir:
|
||||
The directory containing release notes.
|
||||
:returns:
|
||||
The path to the config file in the release notes directory.
|
||||
:rtype:
|
||||
str
|
||||
"""
|
||||
return os.path.join(relnotesdir, defaults.RELEASE_NOTES_CONFIG_FILENAME)
|
||||
|
||||
|
||||
def read_config(config_file):
|
||||
"""Read and parse the config file.
|
||||
|
||||
:param str config_file:
|
||||
The path to the config file to parse.
|
||||
:returns:
|
||||
The YAML parsed into a dictionary, otherwise, or an empty dictionary
|
||||
if the path does not exist.
|
||||
:rtype:
|
||||
dict
|
||||
"""
|
||||
if not os.path.exists(config_file):
|
||||
return {}
|
||||
|
||||
with open(config_file, 'r') as fd:
|
||||
return yaml.safe_load(fd)
|
||||
|
||||
|
||||
def parse_config_into(parsed_arguments):
|
||||
"""Parse the user config onto the namespace arguments.
|
||||
|
||||
:param parsed_arguments:
|
||||
The result of calling :meth:`argparse.ArgumentParser.parse_args`.
|
||||
:type parsed_arguments:
|
||||
argparse.Namespace
|
||||
"""
|
||||
config_path = get_config_path(parsed_arguments.relnotesdir)
|
||||
config_values = read_config(config_path)
|
||||
|
||||
for key in config_values.keys():
|
||||
try:
|
||||
getattr(parsed_arguments, key)
|
||||
except AttributeError:
|
||||
LOG.info('Option "%s" does not apply to this particular command.'
|
||||
'. Ignoring...', key)
|
||||
continue
|
||||
setattr(parsed_arguments, key, config_values[key])
|
||||
|
||||
parsed_arguments._config = config_values
|
|
@ -12,3 +12,4 @@
|
|||
|
||||
RELEASE_NOTES_SUBDIR = 'releasenotes'
|
||||
NOTES_SUBDIR = 'notes'
|
||||
RELEASE_NOTES_CONFIG_FILENAME = 'config.yml'
|
||||
|
|
|
@ -32,6 +32,7 @@ def list_cmd(args):
|
|||
branch=args.branch,
|
||||
collapse_pre_releases=collapse,
|
||||
earliest_version=args.earliest_version,
|
||||
config=args._config
|
||||
)
|
||||
if args.version:
|
||||
versions = args.version
|
||||
|
|
|
@ -16,6 +16,7 @@ import os.path
|
|||
import six
|
||||
import yaml
|
||||
|
||||
from reno import config
|
||||
from reno import scanner
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -31,7 +32,8 @@ class Loader(object):
|
|||
def __init__(self, reporoot, notesdir, branch=None,
|
||||
collapse_pre_releases=True,
|
||||
earliest_version=None,
|
||||
ignore_cache=False):
|
||||
ignore_cache=False,
|
||||
parsedconfig=None):
|
||||
"""Initialize a Loader.
|
||||
|
||||
The versions are presented in reverse chronological order.
|
||||
|
@ -52,13 +54,22 @@ class Loader(object):
|
|||
:type earliest_version: str
|
||||
:param ignore_cache: Do not load a cache file if it is present.
|
||||
:type ignore_cache: bool
|
||||
|
||||
:param parsedconfig: Parsed configuration from file
|
||||
:type parsedconfig: dict
|
||||
"""
|
||||
self._reporoot = reporoot
|
||||
self._notesdir = notesdir
|
||||
self._branch = branch
|
||||
self._collapse_pre_releases = collapse_pre_releases
|
||||
self._earliest_version = earliest_version
|
||||
self._config = parsedconfig
|
||||
if parsedconfig is None:
|
||||
# NOTE(sigmavirus24): This should only happen when it is being
|
||||
# used by the reStructuredText directive in our sphinx extension.
|
||||
notesconfig = config.get_config_path(notesdir)
|
||||
self._config = config.read_config(notesconfig)
|
||||
self._collapse_pre_releases = self._value_from_config(
|
||||
'collapse_pre_releases', collapse_pre_releases, default=True)
|
||||
self._earliest_version = self._value_from_config('earliest_version',
|
||||
earliest_version)
|
||||
self._ignore_cache = ignore_cache
|
||||
|
||||
self._cache = None
|
||||
|
@ -67,6 +78,13 @@ class Loader(object):
|
|||
|
||||
self._load_data()
|
||||
|
||||
def _value_from_config(self, name, value, default=None):
|
||||
# NOTE(sigmavirus24): If it's the default value for the parameter
|
||||
# definition then we might want to look at the config.
|
||||
if value is default:
|
||||
value = self._config.get(name, default)
|
||||
return value
|
||||
|
||||
def _load_data(self):
|
||||
cache_file_exists = os.path.exists(self._cache_filename)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import logging
|
|||
import sys
|
||||
|
||||
from reno import cache
|
||||
from reno import config
|
||||
from reno import create
|
||||
from reno import defaults
|
||||
from reno import lister
|
||||
|
@ -134,7 +135,13 @@ def main(argv=sys.argv[1:]):
|
|||
_build_query_arg_group(do_cache)
|
||||
do_cache.set_defaults(func=cache.cache_cmd)
|
||||
|
||||
args = parser.parse_args()
|
||||
original_args = parser.parse_args(argv)
|
||||
config.parse_config_into(original_args)
|
||||
# NOTE(sigmavirus24): We parse twice to avoid having to guess if a parsed
|
||||
# option is the default value or not. This allows us to apply the config
|
||||
# to the proper command and then make sure that the command-line values
|
||||
# take precedence
|
||||
args = parser.parse_args(argv, original_args)
|
||||
|
||||
logging.basicConfig(
|
||||
level=args.verbosity,
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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 argparse
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
|
||||
from reno import config
|
||||
from reno import defaults
|
||||
from reno.tests import base
|
||||
|
||||
|
||||
class TestConfig(base.TestCase):
|
||||
EXAMPLE_CONFIG = """
|
||||
branch: master
|
||||
collapse_pre_releases: false
|
||||
earliest_version: true
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestConfig, self).setUp()
|
||||
# Temporary directory to store our config
|
||||
self.tempdir = self.useFixture(fixtures.TempDir())
|
||||
|
||||
# Argument parser and parsed arguments for our config function
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--branch')
|
||||
parser.add_argument('--collapse-pre-releases')
|
||||
parser.add_argument('--earliest-version')
|
||||
self.args = parser.parse_args([])
|
||||
self.args.relnotesdir = self.tempdir.path
|
||||
|
||||
config_path = self.tempdir.join(defaults.RELEASE_NOTES_CONFIG_FILENAME)
|
||||
|
||||
with open(config_path, 'w') as fd:
|
||||
fd.write(self.EXAMPLE_CONFIG)
|
||||
|
||||
self.addCleanup(os.unlink, config_path)
|
||||
self.config_path = config_path
|
||||
|
||||
def test_applies_relevant_config_values(self):
|
||||
"""Verify that our config function overrides default values."""
|
||||
config.parse_config_into(self.args)
|
||||
del self.args._config
|
||||
expected_value = {
|
||||
'relnotesdir': self.tempdir.path,
|
||||
'branch': 'master',
|
||||
'collapse_pre_releases': False,
|
||||
'earliest_version': True,
|
||||
}
|
||||
self.assertDictEqual(expected_value, vars(self.args))
|
||||
|
||||
def test_does_not_add_extra_options(self):
|
||||
"""Show that earliest_version is not set when missing."""
|
||||
del self.args.earliest_version
|
||||
self.assertEqual(0, getattr(self.args, 'earliest_version', 0))
|
||||
|
||||
config.parse_config_into(self.args)
|
||||
del self.args._config
|
||||
expected_value = {
|
||||
'relnotesdir': self.tempdir.path,
|
||||
'branch': 'master',
|
||||
'collapse_pre_releases': False,
|
||||
}
|
||||
|
||||
self.assertDictEqual(expected_value, vars(self.args))
|
||||
|
||||
def test_get_congfig_path(self):
|
||||
"""Show that we generate the path appropriately."""
|
||||
self.assertEqual('releasenotes/config.yml',
|
||||
config.get_config_path('releasenotes'))
|
||||
|
||||
def test_read_config_shortcircuits(self):
|
||||
"""Verify we don't try to open a non-existent file."""
|
||||
self.assertDictEqual({},
|
||||
config.read_config('fake/path/to/config.yml'))
|
||||
|
||||
def test_read_config(self):
|
||||
"""Verify we read and parse the config file specified if it exists."""
|
||||
self.assertDictEqual({'branch': 'master',
|
||||
'collapse_pre_releases': False,
|
||||
'earliest_version': True},
|
||||
config.read_config(self.config_path))
|
Loading…
Reference in New Issue