From 8394fbfd94c4179bf197143ff25d8ae17b10c2e3 Mon Sep 17 00:00:00 2001 From: drewcassidy Date: Fri, 30 Apr 2021 01:52:06 -0700 Subject: [PATCH] Improve version number incrementing by rewriting version module --- CHANGELOG.md | 1 + tests/test_cli.py | 5 ++- yaclog/changelog.py | 4 +-- yaclog/cli/__main__.py | 39 +++++++++++----------- yaclog/version.py | 73 +++++++++++++++++++++++++++--------------- 5 files changed, 74 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b5e0c..fb16c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file ### Changed - improved version header parsing +- improved version number incrementing. It can now handle other text surrounding a pep440-compliant version number, which will not be modified ## 0.3.3 - 2021-04-27 diff --git a/tests/test_cli.py b/tests/test_cli.py index 31d8b4b..69bcdde 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,6 @@ import os.path import unittest +import traceback import git from click.testing import CliRunner @@ -9,7 +10,9 @@ from yaclog.cli.__main__ import cli def check_result(runner, result, success: bool = True): - runner.assertEqual((result.exit_code == 0), success, f'output: {result.output}\ntraceback: {result.exc_info}') + runner.assertEqual((result.exit_code == 0), success, + f'\noutput: {result.output}\ntraceback: ' + ''.join( + traceback.format_exception(*result.exc_info))) class TestCreation(unittest.TestCase): diff --git a/yaclog/changelog.py b/yaclog/changelog.py index 3d4eafc..5cfbfd1 100644 --- a/yaclog/changelog.py +++ b/yaclog/changelog.py @@ -269,7 +269,7 @@ class Changelog: # strip whitespace from header self.header = markdown.join(header_segments) - def write(self, path: os.PathLike = None) -> None: + def write(self, path=None) -> None: """ Write a markdown changelog to a file. @@ -311,7 +311,7 @@ class Changelog: def current_version(self, released: Optional[bool] = None, new_version: bool = False, new_version_name: str = 'Unreleased') -> VersionEntry: """ - Get the current version entry from the changelog + Get the current version from the changelog :param released: if the returned version should be a released version, an unreleased version, or ``None`` to return the most recent diff --git a/yaclog/cli/__main__.py b/yaclog/cli/__main__.py index 28dc020..3095b95 100644 --- a/yaclog/cli/__main__.py +++ b/yaclog/cli/__main__.py @@ -179,33 +179,34 @@ def entry(obj: Changelog, bullets, paragraphs, section_name, version_name): @cli.command(short_help='Release versions.') -@click.option('-v', '--version', 'v_flag', type=str, default=None, help='The new version number to use.') -@click.option('-M', '--major', 'v_flag', flag_value='+M', help='Increment major version number.') -@click.option('-m', '--minor', 'v_flag', flag_value='+m', help='Increment minor version number.') -@click.option('-p', '--patch', 'v_flag', flag_value='+p', help='Increment patch number.') -@click.option('-a', '--alpha', 'v_flag', flag_value='+a', help='Increment alpha version number.') -@click.option('-b', '--beta', 'v_flag', flag_value='+b', help='Increment beta version number.') -@click.option('-r', '--rc', 'v_flag', flag_value='+rc', help='Increment release candidate version number.') +@click.option('-v', '--version', 'version_name', type=str, default=None, help='The new version number to use.') +@click.option('-M', '--major', 'rel_seg', flag_value=0, default=None, help='Increment major version number.') +@click.option('-m', '--minor', 'rel_seg', flag_value=1, help='Increment minor version number.') +@click.option('-p', '--patch', 'rel_seg', flag_value=2, help='Increment patch number.') +@click.option('-a', '--alpha', 'pre_seg', flag_value='a', default=None, help='Increment alpha version number.') +@click.option('-b', '--beta', 'pre_seg', flag_value='b', help='Increment beta version number.') +@click.option('-r', '--rc', 'pre_seg', flag_value='rc', help='Increment release candidate version number.') @click.option('-c', '--commit', is_flag=True, help='Create a git commit tagged with the new version number.') @click.pass_obj -def release(obj: Changelog, v_flag, commit): +def release(obj: Changelog, version_name, rel_seg, pre_seg, commit): """Release versions in the changelog and increment their version numbers""" - matches = [v for v in obj.versions if v.name.lower() != 'unreleased'] - if len(matches) == 0: + try: + version = obj.current_version(released=True).name + except ValueError: version = '0.0.0' - else: - version = matches[0].name - cur_version = obj.versions[0] + cur_version = obj.current_version() + new_name = version old_name = cur_version.name - if v_flag: - if v_flag[0] == '+': - new_name = yaclog.version.increment_version(version, v_flag) - else: - new_name = v_flag + if version_name: + new_name = version_name - if yaclog.version.is_release(cur_version.name): + if rel_seg is not None or pre_seg is not None: + new_name = yaclog.version.increment_version(new_name, rel_seg, pre_seg) + + if new_name != old_name: + if yaclog.version.is_release(old_name): click.confirm(f'Rename release version "{cur_version.name}" to "{new_name}"?', abort=True) cur_version.name = new_name diff --git a/yaclog/version.py b/yaclog/version.py index bbb2762..eb46589 100644 --- a/yaclog/version.py +++ b/yaclog/version.py @@ -14,19 +14,37 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from packaging.version import Version, InvalidVersion +import re +from typing import Optional, Tuple + +from packaging.version import Version, VERSION_PATTERN + +version_regex = re.compile(VERSION_PATTERN, re.VERBOSE | re.IGNORECASE) -def is_release(version: str) -> bool: - try: - v = Version(version) - return not (v.is_devrelease or v.is_prerelease) - except InvalidVersion: - return False +def extract_version(version_str: str) -> Tuple[Optional[Version], int, int]: + """ + Extracts a PEP440 version object from a string which may have other text + + :param version_str: The input string to extract from + :return: A tuple of (version, start, end), where start and end are the span of the version in the original string + """ + match = version_regex.search(version_str) + if not match: + return None, -1, -1 + return (Version(match[0]),) + match.span() -def increment_version(version: str, mode: str) -> str: - v = Version(version) +def increment_version(version_str: str, rel_seg: int = None, pre_seg: str = None) -> str: + """ + Increment the PEP440 version number in a string + + :param version_str: The input string to manipulate + :param rel_seg: Which segment of the "release" value to increment, if any + :param pre_seg: Which kind of prerelease to use, if any + :return: The original string with the version number incremented + """ + v, *span = extract_version(version_str) epoch = v.epoch release = v.release pre = v.pre @@ -34,27 +52,23 @@ def increment_version(version: str, mode: str) -> str: dev = v.dev local = v.local - if mode == '+M': - release = (release[0] + 1,) + ((0,) * len(release[1:])) - pre = post = dev = None - elif mode == '+m': - release = (release[0], release[1] + 1) + ((0,) * len(release[2:])) - pre = post = dev = None - elif mode == '+p': - release = (release[0], release[1], release[2] + 1) + ((0,) * len(release[3:])) - pre = post = dev = None - elif mode in ['+a', '+b', '+rc']: - if pre[0] == mode[1:]: - pre = (mode[1:], pre[1] + 1) - else: - pre = (mode[1:], 0) - else: - raise IndexError(f'Unknown mode {mode}') + if rel_seg is not None: + if len(release) <= rel_seg: + release += (0,) * (1 + rel_seg - len(release)) + release = release[0:rel_seg] + (release[rel_seg] + 1,) + (0,) * (len(release) - rel_seg - 1) - return join_version(epoch, release, pre, post, dev, local) + if pre_seg is not None: + if pre and pre[0] == pre_seg: + pre = (pre_seg, pre[1] + 1) + else: + pre = (pre_seg, 1) + + new_v = join_version(epoch, release, pre, post, dev, local) + return version_str[0:span[0]] + new_v + version_str[span[1]:] def join_version(epoch, release, pre, post, dev, local) -> str: + """Join multiple segments of a PEP440 version""" parts = [] # Epoch @@ -83,3 +97,10 @@ def join_version(epoch, release, pre, post, dev, local) -> str: return "".join(parts) +def is_release(version_str: str) -> bool: + """Check if a version string is a release version or not. Returns false if a PEP440 version could not be found""" + v, *span = extract_version(version_str) + if v: + return not (v.is_devrelease or v.is_prerelease) + else: + return False