Browse Source

add changelog entry v2 format

master
Niklas Rosenstein 10 months ago
parent
commit
a31030fd9f
No known key found for this signature in database GPG Key ID: 6D269B33D25F6C6
3 changed files with 76 additions and 45 deletions
  1. + 11
    - 10
      .changelog/_unreleased.yml
  2. + 15
    - 14
      src/shore/__main__.py
  3. + 50
    - 21
      src/shore/util/changelog.py

+ 11
- 10
.changelog/_unreleased.yml

@ -1,12 +1,13 @@
- types:
- improvement
issues: []
components:
- general
- type: improvement
component: general
description: Replace `shore.utils.git` with `nr.utils.git` module
- types:
- change
issues: []
components:
- cli
fixes: []
- type: change
component: cli
description: default value for `shore changelog --for` option is now `general`
fixes: []
- type: change
component: changelog
description: update changelog entry format (`ChangelogEntryV2`), V1 will be automatically
converted to V2 on load
fixes: []

+ 15
- 14
src/shore/__main__.py

@ -34,7 +34,7 @@ from shore.core.plugins import (
from shore.mapper import mapper
from shore.model import Monorepo, ObjectCache, Package, VersionSelector
from shore.plugins.core import get_monorepo_interdependency_version_refs
from shore.util.changelog import ChangelogEntry, ChangelogManager, render_changelogs
from shore.util.changelog import ChangelogEntryV2, ChangelogTypeV2, ChangelogManager, render_changelogs
from shore.util.classifiers import get_classifiers
from shore.util.license import get_license_metadata, wrap_license_text
from shore.util.resources import walk_package_resources
@ -782,13 +782,13 @@ def publish(**args):
@cli.command()
@click.argument('version', type=parse_version, required=False)
@click.option('--reformat', is_flag=True, help='reformat the changelog')
@click.option('--add', metavar='type,…', help='create a new changelog entry')
@click.option('--for', metavar='component,…', help='components for the new changelog entry (default: general)', default='general')
@click.option('--add', metavar='type', help='create a new changelog entry')
@click.option('--for', metavar='component', help='components for the new changelog entry (default: general)', default='general')
@click.option('--fixes', metavar='issue,…', help='issues that this changelog entry fixes')
@click.option('-m', '--message', metavar='text', help='changelog entry description')
@click.option('-e', '--edit', is_flag=True, help='edit the changelog entry or file')
@click.option('--markdown', is_flag=True, help='render the changelog as markdown')
@click.option('-a', '--all', is_flag=True, help='show the changelog for all versions.')
@click.option('-a', '--all', is_flag=True, help='show the changelog for all versions')
def changelog(**args):
"""
Show changelogs or create new entries.
@ -812,16 +812,17 @@ def changelog(**args):
if not args['for']:
args['for'] = 'general'
# Warn about bad changelog types.
for entry_type in _split(args['add']):
if entry_type not in manager.TYPES:
logger.warning('"%s" is not a well-known changelog entry type.', entry_type)
try:
type_ = ChangelogTypeV2[args['add']]
except KeyError:
logger.error('invalid changelog type: %r', args['add'])
sys.exit(1)
entry = ChangelogEntry(
_split(args['add']),
_split(args['fixes']),
_split(args['for']),
args['message'] or '')
entry = ChangelogEntryV2(
type_,
args['for'],
args['message'] or '',
_split(args['fixes']))
# Allow the user to edit the entry if no description is provided or the
# -e,--edit option was set.
@ -830,7 +831,7 @@ def changelog(**args):
entry = mapper.deserialize(yaml.safe_load(_edit_text(serialized)), ChangelogEntry)
# Validate the entry contents (need a description and at least one type and component).
if not entry.types or not entry.description or not entry.components:
if not entry.description or not entry.component:
logger.error('changelog entries need at least one type and component and a description')
sys.exit(1)

+ 50
- 21
src/shore/util/changelog.py

@ -19,11 +19,12 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from nr.databind.core import Field, ObjectMapper, Struct
from nr.databind.core import Field, FieldName, ObjectMapper, Struct
from nr.stream import Stream
from shore.util.version import Version
from termcolor import colored
from typing import Iterable, List, Optional, TextIO
import enum
import os
import re
import shutil
@ -31,12 +32,43 @@ import textwrap
import yaml
class ChangelogEntry(Struct):
class ChangelogEntryV1(Struct):
types = Field([str])
issues = Field([(str, int)], default=list)
components = Field([str])
description = Field(str)
def as_v2(self) -> 'ChangelogEntryV2':
try:
type_ = ChangelogTypeV2[self.types[0].strip().lower()]
except KeyError:
type_ = ChangelogTypeV2.change
return ChangelogEntryV2(
type_,
self.components[0],
self.description,
list(map(str, self.issues)))
class ChangelogTypeV2(enum.Enum):
fix = 0
improvement = 1
change = 3
refactor = 4
feature = 5
docs = 6
tests = 7
class ChangelogEntryV2(Struct):
type_ = Field(ChangelogTypeV2, FieldName('type'))
component = Field(str)
description = Field(str)
fixes = Field([str])
def as_v2(self) -> 'ChangelogEntryV2':
return self
class Changelog:
@ -54,16 +86,18 @@ class Changelog:
def load(self) -> None:
with open(self.filename) as fp:
data = yaml.safe_load(fp)
self.entries = self.mapper.deserialize(data, [ChangelogEntry], filename=self.filename)
datatype = [(ChangelogEntryV2, ChangelogEntryV1)]
self.entries = self.mapper.deserialize(data, datatype, filename=self.filename)
self.entries = [x.as_v2() for x in self.entries]
def save(self, create_directory: bool = False) -> None:
if create_directory:
os.makedirs(os.path.dirname(self.filename), exist_ok=True)
data = self.mapper.serialize(self.entries, [ChangelogEntry])
data = self.mapper.serialize(self.entries, [ChangelogEntryV2])
with open(self.filename, 'w') as fp:
yaml.safe_dump(data, fp, sort_keys=False)
def add_entry(self, entry: ChangelogEntry) -> None:
def add_entry(self, entry: ChangelogEntryV2) -> None:
self.entries.append(entry)
@ -119,7 +153,7 @@ class ChangelogManager:
def _group_entries_by_component(entries):
key = lambda x: x.components[0]
key = lambda x: x.component
return list(Stream.sortby(entries, key).groupby(key, collect=list))
@ -143,17 +177,12 @@ def render_changelogs_for_terminal(fp: TextIO, changelogs: List[Changelog]) -> N
return i
def _fmt_issues(entry):
if not entry.issues:
if not entry.fixes:
return None
return '(' + ', '.join(colored(_fmt_issue(i), 'yellow', attrs=['underline']) for i in entry.issues) + ')'
return '(' + ', '.join(colored(_fmt_issue(i), 'yellow', attrs=['underline']) for i in entry.fixes) + ')'
def _fmt_types(entry):
return ', '.join(colored(f, attrs=['bold']) for f in entry.types)
def _fmt_components(entry):
if len(entry.components) <= 1:
return None
return '(' + ', '.join(colored(f, 'red', attrs=['bold', 'underline']) for f in entry.components[1:]) + ')'
return colored(entry.type_.name, attrs=['bold'])
if hasattr(shutil, 'get_terminal_size'):
width = shutil.get_terminal_size((80, 23))[0]
@ -164,13 +193,13 @@ def render_changelogs_for_terminal(fp: TextIO, changelogs: List[Changelog]) -> N
for changelog in changelogs:
fp.write(colored(changelog.version or 'Unreleased', 'blue', attrs=['bold', 'underline']) + '\n')
for component, entries in _group_entries_by_component(changelog.entries):
maxw = max(len(', '.join(x.types)) for x in entries)
maxw = max(map(lambda x: len(x.type_.name), entries))
fp.write(' ' + colored(component or 'No Component', 'red', attrs=['bold', 'underline']) + '\n')
for entry in entries:
lines = textwrap.wrap(entry.description, width - (maxw + 6))
suffix_fmt = ' '.join(filter(bool, (_fmt_issues(entry), _fmt_components(entry))))
suffix_fmt = ' '.join(filter(bool, (_fmt_issues(entry),)))
lines[-1] += ' ' + suffix_fmt
delta = maxw - len(', '.join(entry.types))
delta = maxw - len(entry.type_.name)
fp.write(' {} {}\n'.format(colored((_fmt_types(entry) + ':') + ' ' * delta, attrs=['bold']), _md_term_stylize(lines[0])))
for line in lines[1:]:
fp.write(' {}{}\n'.format(' ' * (maxw+2), _md_term_stylize(line)))
@ -185,17 +214,17 @@ def render_changelogs_as_markdown(fp: TextIO, changelogs: List[Changelog]) -> No
return i
def _fmt_issues(entry):
if not entry.issues:
if not entry.fixes:
return None
return '(' + ', '.join(_fmt_issue(i) for i in entry.issues) + ')'
return '(' + ', '.join(_fmt_issue(i) for i in entry.fixes) + ')'
for changelog in changelogs:
fp.write('## {}\n\n'.format(changelog.version or 'Unreleased'))
for component, entries in _group_entries_by_component(changelog.entries):
fp.write('* __{}__\n'.format(component))
for entry in entries:
description ='**' + ', '.join(entry.types) + '**: ' + entry.description
if entry.issues:
description ='**' + entry.type_.name + '**: ' + entry.description
if entry.fixes:
description += ' ' + _fmt_issues(entry)
lines = textwrap.wrap(description, 80)
fp.write(' * {}\n'.format(lines[0]))

Loading…
Cancel
Save