reno/reno/loader.py

141 lines
5.2 KiB
Python

# 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 six
import yaml
from reno import scanner
LOG = logging.getLogger(__name__)
def get_cache_filename(reporoot, notesdir):
return os.path.join(reporoot, notesdir, 'reno.cache')
class Loader(object):
"Load the release notes for a given repository."
def __init__(self, conf,
ignore_cache=False):
"""Initialize a Loader.
The versions are presented in reverse chronological order.
Notes files are associated with the earliest version for which
they were available, regardless of whether they changed later.
:param conf: Parsed configuration from file
:type conf: reno.config.Config
:param ignore_cache: Do not load a cache file if it is present.
:type ignore_cache: bool
"""
self._config = conf
self._ignore_cache = ignore_cache
self._reporoot = conf.reporoot
self._notespath = conf.notespath
self._branch = conf.branch
self._collapse_pre_releases = conf.collapse_pre_releases
self._earliest_version = conf.earliest_version
self._cache = None
self._scanner = None
self._scanner_output = None
self._cache_filename = get_cache_filename(self._reporoot,
self._notespath)
self._load_data()
def _load_data(self):
cache_file_exists = os.path.exists(self._cache_filename)
if self._ignore_cache and cache_file_exists:
LOG.debug('ignoring cache file %s', self._cache_filename)
if (not self._ignore_cache) and cache_file_exists:
with open(self._cache_filename, 'r') as f:
self._cache = yaml.safe_load(f.read())
# Save the cached scanner output to the same attribute
# it would be in if we had loaded it "live". This
# simplifies some of the logic in the other methods.
self._scanner_output = {
n['version']: n['files']
for n in self._cache['notes']
}
else:
self._scanner = scanner.Scanner(self._config)
self._scanner_output = self._scanner.get_notes_by_version()
@property
def versions(self):
"A list of all of the versions found."
return list(self._scanner_output.keys())
def __getitem__(self, version):
"Return data about the files that should go into a given version."
return self._scanner_output[version]
def parse_note_file(self, filename, sha):
"""Return the data structure encoded in the note file.
Emit warnings for content that does not look valid in some
way, but return it anyway for backwards-compatibility.
"""
if self._cache:
content = self._cache['file-contents'][filename]
else:
body = self._scanner.get_file_at_commit(filename, sha)
content = yaml.safe_load(body)
cleaned_content = {}
for section_name, section_content in content.items():
if section_name == 'prelude':
if not isinstance(section_content, six.string_types):
LOG.warning(
('The prelude section of %s '
'does not parse as a single string. '
'Is the YAML input escaped properly?') %
filename,
)
else:
if isinstance(section_content, six.string_types):
# A single string is OK, but wrap it with a list
# so the rest of the code can treat the data model
# consistently.
section_content = [section_content]
elif not isinstance(section_content, list):
LOG.warning(
('The %s section of %s '
'does not parse as a string or list of strings. '
'Is the YAML input escaped properly?') % (
section_name, filename),
)
else:
for item in section_content:
if not isinstance(item, six.string_types):
LOG.warning(
('The item %r in the %s section of %s '
'parses as a %s instead of a string. '
'Is the YAML input escaped properly?'
) % (item, section_name,
filename, type(item)),
)
cleaned_content[section_name] = section_content
return cleaned_content