From a230968736dac8e40661e51fdaa361665afb091d Mon Sep 17 00:00:00 2001 From: drewcassidy Date: Thu, 6 May 2021 22:23:35 -0700 Subject: [PATCH] `header` attribute on the changelog class has been split into `title` and `preamble` --- CHANGELOG.md | 1 + docs/handbook/changelog_files.md | 8 +++- tests/common.py | 3 +- tests/test_changelog.py | 12 ++++-- yaclog/changelog.py | 66 ++++++++++++++++++++------------ yaclog/cli/__main__.py | 2 +- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b0652..a8e6ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file - improved version number incrementing in `release`. - can now handle other text surrounding a pep440-compliant version number, which will not be modified - can now handle pre-releases correctly. The version to increment is the most recent version in the log with a valid pep440 version number in it. Release increment and prerelease increments can be mixed, allowing e.g: `yaclog release -mr` to create a release candidate with in incremented minor version number. +- `header` attribute on the changelog class has been split into `title` and `preamble` ### Removed diff --git a/docs/handbook/changelog_files.md b/docs/handbook/changelog_files.md index 5afd80c..7011d8b 100644 --- a/docs/handbook/changelog_files.md +++ b/docs/handbook/changelog_files.md @@ -2,9 +2,13 @@ Yaclog works on markdown changelog files, using a machine-readable format based on what is proposed by [Keep a Changelog](https://keepachangelog.com). Changelog files can be created using the {command}`yaclog init` command. -## Front Matter +## Title -The front matter, or header, of a changelog is the text at the top of the file before any version information. It can contain the file's title, an explanation of the file's purpose, as well as any general machine-readable information you may want to include for use with other tools. Yaclog does not provide any ways to manipulate the front matter from the command line due to its open-ended nature. +The title is the first H1 in the file giving its title, usually `# Changlog`. + +## Preamble + +The preamble is the text at the top of the file before any version information. It can contain an explanation of the file's purpose, as well as any general machine-readable information you may want to include for use with other tools. Yaclog does not provide any ways to manipulate the front matter from the command line due to its open-ended nature. ## Versions diff --git a/tests/common.py b/tests/common.py index 6868987..088b9e6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -46,7 +46,8 @@ log_segments = [ log_text = '\n\n'.join(log_segments) log = yaclog.Changelog() -log.header = '# Changelog\n\nThis changelog is for testing the parser, and has many things in it that might trip it up.' +log.title = 'Changelog' +log.preamble = ['This changelog is for testing the parser, and has many things in it that might trip it up.'] log.links = {'id': 'http://www.koalastothemax.com'} log.versions = [yaclog.changelog.VersionEntry(), yaclog.changelog.VersionEntry(), yaclog.changelog.VersionEntry()] diff --git a/tests/test_changelog.py b/tests/test_changelog.py index 99a75e2..c203df0 100644 --- a/tests/test_changelog.py +++ b/tests/test_changelog.py @@ -22,9 +22,13 @@ class TestParser(unittest.TestCase): """Test the log's path""" self.assertEqual(self.path, self.log.path) - def test_header(self): - """Test the header information at the top of the file""" - self.assertEqual(log.header, self.log.header) + def test_title(self): + """Test the title at the top of the file""" + self.assertEqual(log.title, self.log.title) + + def test_preamble(self): + """Test the preamble at the top of the file""" + self.assertEqual(log.preamble, self.log.preamble) def test_links(self): """Test the links at the end of the file""" @@ -56,7 +60,7 @@ class TestWriter(unittest.TestCase): def test_header(self): """Test the header information at the top of the file""" - self.assertEqual(log_segments[0:2], self.log_segments[0:2]) + self.assertEqual(log_segments[1], self.log_segments[1]) def test_links(self): """Test the links at the end of the file""" diff --git a/yaclog/changelog.py b/yaclog/changelog.py index 668aa7f..4f60308 100644 --- a/yaclog/changelog.py +++ b/yaclog/changelog.py @@ -31,8 +31,6 @@ import click # only for styling import yaclog.markdown as markdown import yaclog.version -default_header = '# Changelog\n\nAll notable changes to this project will be documented in this file' - class VersionEntry: """Holds a single version entry in a :py:class:`Changelog`""" @@ -219,18 +217,24 @@ class VersionEntry: class Changelog: """A changelog made up of a header, several versions, and a link table""" - def __init__(self, path=None, header: str = default_header): + def __init__(self, path=None, + title: str = 'Changelog', + preamble: str = "All notable changes to this project will be documented in this file"): """ Contents will be automatically read from disk if the file exists :param path: The changelog's path on disk - :param header: The header at the top of the changelog to use if the file does not exist + :param str title: The changelog title to use if the file does not exist. + :param str preamble: The changelog preamble to use if the file does not exist. """ self.path = os.path.abspath(path) if path else None """The path of the changelog's file on disk""" - self.header: str = header - """Any text at the top of the changelog before any H2s""" + self.title: str = title + """The title of the changelog""" + + self.preamble: List[str] = preamble + """Any text at the top of the changelog before any version information as a list of paragraphs""" self.versions: List[VersionEntry] = [] """A list of versions in the changelog""" @@ -243,7 +247,8 @@ class Changelog: def read(self, path=None) -> None: """ - Read a markdown changelog file from disk + Read a markdown changelog file from disk. The object's contents will be overwritten by the file contents if + reading is successful. :param path: The changelog's path on disk. By default, :py:attr:`~Changelog.path` is used. """ @@ -254,50 +259,57 @@ class Changelog: # Read file with open(path, 'r') as fp: - tokens, self.links = markdown.tokenize(fp.read()) + tokens, links = markdown.tokenize(fp.read()) section = '' - header_segments = [] + versions = [] + title = None + preamble = [] for token in tokens: text = '\n'.join(token.lines) if token.kind == 'h2': # start of a version - self.versions.append(VersionEntry.from_header(text, line_no=token.line_no)) + versions.append(VersionEntry.from_header(text, line_no=token.line_no)) section = '' - elif len(self.versions) == 0: + elif len(versions) == 0: # we haven't encountered any version headers yet, - # so its best to just add this line to the header string - header_segments.append(text) + # so its best to just add this line to the preamble or title + if token.kind == 'h1' and not title: + title = text.strip('#').strip() + else: + preamble.append(text) elif token.kind == 'h3': # start of a version section section = text.strip('#').strip() - if section not in self.versions[-1].sections.keys(): - self.versions[-1].sections[section] = [] + if section not in versions[-1].sections.keys(): + versions[-1].sections[section] = [] else: # change log entry - self.versions[-1].sections[section].append(text) + versions[-1].sections[section].append(text) # handle links - for version in self.versions: + for version in versions: if match := re.fullmatch(r'\[(.*)]', version.name): # ref-matched link link_id = match[1].lower() - if link_id in self.links: - version.link = self.links[link_id] + if link_id in links: + version.link = links[link_id] version.link_id = None version.name = match[1] - elif version.link_id in self.links: + elif version.link_id in links: # id-matched link - version.link = self.links[version.link_id] + version.link = links[version.link_id] - # strip whitespace from header - self.header = markdown.join(header_segments) + self.title = title + self.preamble = preamble + self.versions = versions + self.links = links def write(self, path=None) -> None: """ @@ -310,7 +322,13 @@ class Changelog: # use the object path if none was provided path = self.path - segments = [self.header] + segments = [] + + if self.title: + segments.append(f'# {self.title}') + if self.preamble: + segments += self.preamble + v_links = {**self.links} for version in self.versions: diff --git a/yaclog/cli/__main__.py b/yaclog/cli/__main__.py index ad98c50..c5ab22f 100644 --- a/yaclog/cli/__main__.py +++ b/yaclog/cli/__main__.py @@ -66,7 +66,7 @@ def reformat(obj: Changelog): help='Show version header and body.') @click.option('--name', '-n', 'str_func', flag_value=lambda v, k: v.name, help='Show only the version name') @click.option('--body', '-b', 'str_func', flag_value=lambda v, k: v.body(**k), help='Show only the version body.') -@click.option('--header', '-h', 'str_func', flag_value=lambda v, k: v.header(**k), help='Show only the version header.') +@click.option('--header', '-h', 'str_func', flag_value=lambda v, k: v.preamble(**k), help='Show only the version header.') @click.argument('version_names', metavar='VERSIONS', type=str, nargs=-1) @click.pass_obj def show(obj: Changelog, all_versions, markdown, str_func, version_names):