# 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(conf): return os.path.normpath(os.path.join( conf.reporoot, conf.notespath, '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(conf) 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 == self._config.prelude_section_name: if not isinstance(section_content, six.string_types): LOG.warning( ('The %s section of %s ' 'does not parse as a single string. ' 'Is the YAML input escaped properly?') % (self._config.prelude_section_name, 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