From f45be890f551dd035931665a57de870e85e0cbbf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 10:39:27 +0000 Subject: [PATCH 01/26] Bump twine from 1.11 to 2.0.0 (#27) Bumps [twine](https://github.com/pypa/twine) from 1.11 to 2.0.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/master/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/1.11.0...2.0.0) Signed-off-by: dependabot-preview[bot] --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index bc2cda5..aed1061 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,3 +1,3 @@ GitPython -twine==1.11 +twine==2.0.0 setuptools \ No newline at end of file From e51b0d28064cf80e2f93e23ca83be89d50cb2d54 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2019 10:40:00 +0000 Subject: [PATCH 02/26] Bump pytest from 5.2.3 to 5.2.4 (#26) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.3 to 5.2.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.2.3...5.2.4) Signed-off-by: dependabot-preview[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 2764687..d8ed703 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ six = "*" coverage = "*" # locked to protect from Py2/3 compatibility break in more recent versions towncrier = "*" -pytest = "==5.2.3" +pytest = "==5.2.4" pytest-cov = "==2.8.1" pytest-html = "==2.0.0" pyautoversion = {path = ".",editable = true} From 41815258d58d1b5d214621b52ecd55d3e967e263 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2019 15:08:35 +0000 Subject: [PATCH 03/26] Bump twine from 2.0.0 to 3.0.0 (#29) Bumps [twine](https://github.com/pypa/twine) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/master/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/2.0.0...3.0.0) Signed-off-by: dependabot-preview[bot] --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index aed1061..102941f 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,3 +1,3 @@ GitPython -twine==2.0.0 +twine==3.0.0 setuptools \ No newline at end of file From 1a740e10498922d8c1f721d9a87ce1a77c1315ad Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2019 15:11:55 +0000 Subject: [PATCH 04/26] Bump pytest-html from 2.0.0 to 2.0.1 (#28) Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/pytest-dev/pytest-html/releases) - [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-html/compare/v2.0.0...v2.0.1) Signed-off-by: dependabot-preview[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index d8ed703..fbf7bab 100644 --- a/Pipfile +++ b/Pipfile @@ -17,7 +17,7 @@ coverage = "*" towncrier = "*" pytest = "==5.2.4" pytest-cov = "==2.8.1" -pytest-html = "==2.0.0" +pytest-html = "==2.0.1" pyautoversion = {path = ".",editable = true} [requires] From 5eed26b122f8970a1f66bff953df71b30da52544 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2019 14:21:58 +0000 Subject: [PATCH 05/26] Bump pytest from 5.2.4 to 5.3.0 (#30) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.4 to 5.3.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.2.4...5.3.0) Signed-off-by: dependabot-preview[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index fbf7bab..62769b3 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ six = "*" coverage = "*" # locked to protect from Py2/3 compatibility break in more recent versions towncrier = "*" -pytest = "==5.2.4" +pytest = "==5.3.0" pytest-cov = "==2.8.1" pytest-html = "==2.0.1" pyautoversion = {path = ".",editable = true} From 7a41143adf6c16b51bbe3b1fecdb5a966f5a323e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2019 10:27:07 +0000 Subject: [PATCH 06/26] Bump twine from 3.0.0 to 3.1.0 (#31) Bumps [twine](https://github.com/pypa/twine) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/master/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/3.0.0...3.1.0) Signed-off-by: dependabot-preview[bot] --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 102941f..3a17a88 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,3 +1,3 @@ GitPython -twine==3.0.0 +twine==3.1.0 setuptools \ No newline at end of file From 33250c07d2f1e6ed55be61b88ec5a9a3c2e39515 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 15:08:09 +0000 Subject: [PATCH 07/26] Bump pytest from 5.3.0 to 5.3.2 (#33) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.0 to 5.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.0...5.3.2) Signed-off-by: dependabot-preview[bot] --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 62769b3..32c12b9 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ six = "*" coverage = "*" # locked to protect from Py2/3 compatibility break in more recent versions towncrier = "*" -pytest = "==5.3.0" +pytest = "==5.3.2" pytest-cov = "==2.8.1" pytest-html = "==2.0.1" pyautoversion = {path = ".",editable = true} From b8c4bb0bca47f862a314cca616bf271d8a23eea1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2020 14:48:03 +0000 Subject: [PATCH 08/26] Bump twine from 3.1.0 to 3.1.1 (#34) Bumps [twine](https://github.com/pypa/twine) from 3.1.0 to 3.1.1. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/master/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/3.1.0...3.1.1) Signed-off-by: dependabot-preview[bot] --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 3a17a88..406c9d0 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,3 +1,3 @@ GitPython -twine==3.1.0 +twine==3.1.1 setuptools \ No newline at end of file From 5e6e9e67b62d782566e685d4a04e55db2b26c9c8 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 9 Jan 2020 14:57:08 +0000 Subject: [PATCH 09/26] Update usage information --- README.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4bdf782..ab3ef1e 100644 --- a/README.md +++ b/README.md @@ -30,32 +30,48 @@ Check your target files, and if they are as you'd expect then you're good to go. For more details about how to use the tool, have a look at the [usage page](./USAGE.md) ``` -usage: auto_version [-h] [--target TARGET] [--bump {major,minor,patch}] - [--news] [--set SET] [--set-patch-count] [--lock] - [--release] [--version] [--config CONFIG] [-v] +usage: auto_version [-h] [--show] + [--bump {major,minor,patch,prerelease,build}] [--news] + [--print-file-triggers] [--set SET] + [--commit-count-as {major,minor,patch,prerelease,build}] + [--lock] [--release] [--version] + [--persist-from {vcs,vcs-latest,source}] + [--persist-to {vcs,source}] [--config CONFIG] [-v] -auto version: a tool to control version numbers +auto version v1.2.0: a tool to control version numbers optional arguments: -h, --help show this help message and exit - --target TARGET Files containing version info. Assumes unique variable - names between files. (default: ['src\\_version.py']). - --bump {major,minor,patch} + --show, --dry-run Don't write anything to disk or vcs. + --bump {major,minor,patch,prerelease,build} Bumps the specified part of SemVer string. Use this locally to correctly modify the version file. --news, --file-triggers Detects need to bump based on presence of files (as specified in config). + --print-file-triggers + Prints a newline separated list of files detected as + bump triggers. --set SET Set the SemVer string. Use this locally to set the project version explicitly. - --set-patch-count Sets the patch number to the commit count. + --commit-count-as {major,minor,patch,prerelease,build} + Use the commit count to set the value of the specified + field. --lock Locks the SemVer string. Lock will remain for another call to autoversion before being cleared. --release Marks as a release build, which flags the build as released. --version Prints the version of auto_version itself (self- version). - --config CONFIG Configuration file path. + --persist-from {vcs,vcs-latest,source} + Where the current version is stored. Looks for each + source in order. (default: source files) + --persist-to {vcs,source} + Where the new version is stored. This could be in + multiple places at once. (default: source files) + --config CONFIG Configuration file path. (default: + C:\Users\adrcab01\OneDrive - + Arm\Documents\GitHub\mbed-targets\pyproject.toml). -v, --verbosity increase output verbosity. can be specified multiple times ``` From 4ba7ee42ea2a694e9cba80739689fe08717e8eb6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2020 09:40:17 +0100 Subject: [PATCH 10/26] Bump pytest from 5.3.2 to 5.4.1 (#48) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.2 to 5.4.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.2...5.4.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 32c12b9..5e6afb4 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ six = "*" coverage = "*" # locked to protect from Py2/3 compatibility break in more recent versions towncrier = "*" -pytest = "==5.3.2" +pytest = "==5.4.1" pytest-cov = "==2.8.1" pytest-html = "==2.0.1" pyautoversion = {path = ".",editable = true} From 0570b17716b3abe15a7f0091c0473ff3f0e9ec56 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2020 15:09:30 +0100 Subject: [PATCH 11/26] Bump pytest-html from 2.0.1 to 2.1.1 (#49) Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 2.0.1 to 2.1.1. - [Release notes](https://github.com/pytest-dev/pytest-html/releases) - [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/CHANGES.rst) - [Commits](https://github.com/pytest-dev/pytest-html/compare/v2.0.1...v2.1.1) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 5e6afb4..6d17e89 100644 --- a/Pipfile +++ b/Pipfile @@ -17,7 +17,7 @@ coverage = "*" towncrier = "*" pytest = "==5.4.1" pytest-cov = "==2.8.1" -pytest-html = "==2.0.1" +pytest-html = "==2.1.1" pyautoversion = {path = ".",editable = true} [requires] From 994883df188027879090ba0203607d689dcc8d72 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Wed, 12 Aug 2020 15:45:45 +0100 Subject: [PATCH 12/26] Complete overhaul (#51) * add a failing test for prerelease increment * add precommit definition * add precommit hooks file * move to semver library to support incrementing prerelease and build flags * move semver requirement to requirements.txt * fix a couple of issues (conflicting versions check was wrong) add a couple of extra tests have to clear the config as it's operating as a global with mutable dicts - fine in CLI but breaks in tests * support for detecting newsfiles added since last release * add ability to extract the names of file triggers * allow multiple sources gives better handling of situation where user is starting from `0.0.0` with no tags. * fix bug where git was returning files relative to project root whereas glob returns files relative to cwd * major: invert news trigger config to allow many-one example config for `trigger_patterns`: ``` "newsfiles/*.bugfix": "patch", "newsfiles/*.doc": "patch", "newsfiles/*.feature": "minor", "newsfiles/*.major": "major", "newsfiles/*.minor": "minor", "newsfiles/*.patch": "patch", "newsfiles/*.removal": "major", ``` * major: invert news trigger config to allow many-one example config for `trigger_patterns`: ``` "newsfiles/*.bugfix": "patch", "newsfiles/*.doc": "patch", "newsfiles/*.feature": "minor", "newsfiles/*.major": "major", "newsfiles/*.minor": "minor", "newsfiles/*.patch": "patch", "newsfiles/*.removal": "major", ``` * provide a persist-from for previous release * add functions for comparing semvers * oops, fix test for trigger config * add previous-release comparison logic * oops, fix test for trigger config * tests and code for correctly incrementing, given a previous release * use the right param name in the cli * log which previous release was detected * minimum patch increment for an existing version * add support for yaml/yml * better yaml * don't perform replacement if line key isn't used * 'ancestor' tag lookup is more accurate changes the cli * match the 'incr_from_release' to the vcs mode (previous or latest) * move update strings into main. print the right version to stdout. Previously, the `new_version` was printed, which included prerelease tags, even if the `--release` flag was passed. This seemed wrong, but does change the behaviour - if the prerelease version is needed on stdout, don't pass the `--release` flag. * Rename inv-conf.major to 3.major * add git config for ci test run yuk so much duplication in this file * fix the tag replacement placeholder previously, tag templates like `v{version}` would end up looking like `^(.*)v$` Because the `10*v` is position-invariant and `.replace` replaces the first match. Using a position-specific string (like, any string with a variety of characters ...) resolves this issue. * add tests and further improve the regex * fix the tag replacement placeholder previously, tag templates like `v{version}` would end up looking like `^(.*)v$` Because the `10*v` is position-invariant and `.replace` replaces the first match. Using a position-specific string (like, any string with a variety of characters ...) resolves this issue. * add tests and further improve the regex Co-authored-by: David Hyman Co-authored-by: David Hyman <5198267+davidhyman@users.noreply.github.com> --- .circleci/config.yml | 4 + docs/news/3.major | 2 + docs/news/4.major | 2 + docs/news/5.feature | 1 + docs/news/6.feature | 2 + docs/news/7.major | 2 + scripts/release.py | 120 ++++----- src/auto_version/auto_version_tool.py | 204 ++++++++++----- src/auto_version/cli.py | 11 +- src/auto_version/config.py | 17 +- src/auto_version/replacement_handler.py | 27 +- src/auto_version/tests/example.toml | 2 +- src/auto_version/tests/test_autoversion.py | 275 ++++++++++++++++++--- src/auto_version/utils.py | 104 ++++++-- 14 files changed, 570 insertions(+), 203 deletions(-) create mode 100644 docs/news/3.major create mode 100644 docs/news/4.major create mode 100644 docs/news/5.feature create mode 100644 docs/news/6.feature create mode 100644 docs/news/7.major diff --git a/.circleci/config.yml b/.circleci/config.yml index db7f071..29afa2c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,6 +44,8 @@ jobs: - run: name: run tests command: | + git config user.email "you@example.com" + git config user.name "Your Name" pipenv run pytest --junitxml=test-reports/results.xml pipenv run green -r src - run: @@ -72,6 +74,8 @@ jobs: name: install dependencies # TODO: consider using pipenv command: | + git config user.email "you@example.com" + git config user.name "Your Name" sudo pip install -U pip pipenv sudo pip install -r scripts/requirements.txt pipenv install --dev . diff --git a/docs/news/3.major b/docs/news/3.major new file mode 100644 index 0000000..359682d --- /dev/null +++ b/docs/news/3.major @@ -0,0 +1,2 @@ +Breaking change to the config spec: the trigger_patterns are specified as pattern:sigfig +in order to support multiple file patterns for each significant figure diff --git a/docs/news/4.major b/docs/news/4.major new file mode 100644 index 0000000..48e5826 --- /dev/null +++ b/docs/news/4.major @@ -0,0 +1,2 @@ +Reworked the DVCS persistence to load from previous tags either globally, or previous to the current commit. +The CLI `--persist-from` options were renamed to accommodate this. \ No newline at end of file diff --git a/docs/news/5.feature b/docs/news/5.feature new file mode 100644 index 0000000..49d362a --- /dev/null +++ b/docs/news/5.feature @@ -0,0 +1 @@ +Adds better workflow for incrementing patches. \ No newline at end of file diff --git a/docs/news/6.feature b/docs/news/6.feature new file mode 100644 index 0000000..b7ae49b --- /dev/null +++ b/docs/news/6.feature @@ -0,0 +1,2 @@ +Adds the ability to have "infinite" newsfiles. They no longer require cleaning up, if tags are used to indicate releases. + This requires a workflow where releases are tagged in git, so we can determine the "new news". \ No newline at end of file diff --git a/docs/news/7.major b/docs/news/7.major new file mode 100644 index 0000000..8482ce8 --- /dev/null +++ b/docs/news/7.major @@ -0,0 +1,2 @@ +Breaking change to the config spec: the trigger_patterns are specified as pattern:sigfig +in order to support multiple file patterns for each significant figure \ No newline at end of file diff --git a/scripts/release.py b/scripts/release.py index edf3e75..ede1d90 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,52 +1,46 @@ -import os - import datetime import logging +import os import subprocess import sys -from git import Repo, Actor -ENVVAR_TWINE_USERNAME = 'TWINE_USERNAME' +from git import Actor +from git import Repo + +ENVVAR_TWINE_USERNAME = "TWINE_USERNAME" NUMBER_OF_RETRIES = 5 -ENVVAR_TWINE_REPOSITORY = 'TWINE_REPOSITORY' +ENVVAR_TWINE_REPOSITORY = "TWINE_REPOSITORY" -ENVVAR_TWINE_REPOSITORY_URL = 'TWINE_REPOSITORY_URL' +ENVVAR_TWINE_REPOSITORY_URL = "TWINE_REPOSITORY_URL" ENVVAR_BRANCH_NAME = "CIRCLE_BRANCH" ENVVAR_GITHUB_TOKEN = "GH_TOKEN" ENVVAR_GITHUB_TOKEN2 = "GITHUB_TOKEN" -REPO_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) +REPO_ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), "..")) def change_remote(repo, github_token, branch_name): origin_url = repo.remotes.origin.url - path = origin_url.split('github.com', 1)[1][1:].strip() - new = 'https://{GITHUB_TOKEN}:x-oauth-basic@github.com/%s' % path - logging.info('Rewriting git remote url to: %s' % new) + path = origin_url.split("github.com", 1)[1][1:].strip() + new = "https://{GITHUB_TOKEN}:x-oauth-basic@github.com/%s" % path + logging.info("Rewriting git remote url to: %s" % new) repo.delete_remote(repo.remotes.origin) - new_origin = repo.create_remote('origin', - url=new.format(GITHUB_TOKEN=github_token) - ) + new_origin = repo.create_remote("origin", url=new.format(GITHUB_TOKEN=github_token)) repo.git.fetch() repo.git.checkout(branch_name) - repo.git.branch( - '--set-upstream-to', '%s/%s' % (new_origin, branch_name) - ) + repo.git.branch("--set-upstream-to", "%s/%s" % (new_origin, branch_name)) def commit_release(repo, branch_name, version): logging.info("Committing release...") - modified_files = [f.a_path for f in - repo.index.diff(None)] + repo.untracked_files + modified_files = [f.a_path for f in repo.index.diff(None)] + repo.untracked_files if len(modified_files) == 0: - logging.info( - "No commit to perform as no changes were detected" - ) + logging.info("No commit to perform as no changes were detected") return - repo.git.add('./src/auto_version/__version__.py') - repo.git.add('CHANGELOG.md') - repo.git.add('./docs/news/*') + repo.git.add("./src/auto_version/__version__.py") + repo.git.add("CHANGELOG.md") + repo.git.add("./docs/news/*") repo.git.tag() # Create commit @@ -54,13 +48,11 @@ def commit_release(repo, branch_name, version): repo.config_writer().set_value("user", "name", author.name).release() repo.config_writer().set_value("user", "email", author.email).release() repo.index.commit( - ":checkered_flag: :newspaper: Releasing version %s @ %s\n[ci skip]" % ( - version, - datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M") - ), - author=author + ":checkered_flag: :newspaper: Releasing version %s @ %s\n[ci skip]" + % (version, datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M")), + author=author, ) - repo.create_tag(version, ref=branch_name, message='Release %s' % version) + repo.create_tag(version, ref=branch_name, message="Release %s" % version) retries = NUMBER_OF_RETRIES while retries > 0: get_latest(repo, branch_name) @@ -68,12 +60,12 @@ def commit_release(repo, branch_name, version): return retries = retries - 1 raise Exception("Failed committing new release") - mark_release_as_latest(branch_name, repo) def mark_release_as_latest(branch_name, repo): - logging.info('Marking this commit as latest') - repo.create_tag('latest', force=True) + logging.info("Marking this commit as latest") + repo.create_tag("latest", force=True) + retries = NUMBER_OF_RETRIES while retries > 0: get_latest(repo, branch_name) if push_to_github(branch_name, repo, force=True): @@ -86,12 +78,11 @@ def push_to_github(branch_name, repo, force=False): logging.info("Pushing back to GitHub...") try: if force: - repo.git.push('-f', '--set-upstream', repo.remotes.origin, - branch_name) + repo.git.push("-f", "--set-upstream", repo.remotes.origin, branch_name) else: - repo.git.push('--follow-tags', '--set-upstream', - repo.remotes.origin, - branch_name) + repo.git.push( + "--follow-tags", "--set-upstream", repo.remotes.origin, branch_name + ) return True except Exception as e: logging.error("Push failed: %s" % str(e)) @@ -106,23 +97,30 @@ def get_latest(repo, branch_name): def get_new_version(): - version = subprocess.check_output( - ['python', 'setup.py', '--version']).decode().strip() - if 'dev' in version: - raise Exception('cannot release unversioned project: %s' % version) + version = ( + subprocess.check_output(["python", "setup.py", "--version"]).decode().strip() + ) + if "dev" in version: + raise Exception("cannot release unversioned project: %s" % version) return version def release_to_pypi(twine_repo, twine_username): - logging.info('Releasing to %s as %s' % (twine_repo, twine_username)) - logging.info('Generating a release package') - subprocess.check_call( - ['python', 'setup.py', 'clean', '--all', - 'bdist_wheel', - '--dist-dir', 'release-dist']) - logging.info('Uploading to PyPI') + logging.info("Releasing to %s as %s" % (twine_repo, twine_username)) + logging.info("Generating a release package") subprocess.check_call( - ['python', '-m', 'twine', 'upload', 'release-dist/*']) + [ + "python", + "setup.py", + "clean", + "--all", + "bdist_wheel", + "--dist-dir", + "release-dist", + ] + ) + logging.info("Uploading to PyPI") + subprocess.check_call(["python", "-m", "twine", "upload", "release-dist/*"]) def get_current_branch(repo): @@ -137,26 +135,28 @@ def get_current_branch(repo): def main(): - gh_token = os.getenv(ENVVAR_GITHUB_TOKEN) or os.getenv( - ENVVAR_GITHUB_TOKEN2) + gh_token = os.getenv(ENVVAR_GITHUB_TOKEN) or os.getenv(ENVVAR_GITHUB_TOKEN2) # see: # https://packaging.python.org/tutorials/distributing-packages/#uploading-your-project-to-pypi - twine_repo = os.getenv('%s' % ENVVAR_TWINE_REPOSITORY_URL) or os.getenv( - ENVVAR_TWINE_REPOSITORY) + twine_repo = os.getenv("%s" % ENVVAR_TWINE_REPOSITORY_URL) or os.getenv( + ENVVAR_TWINE_REPOSITORY + ) twine_username = os.getenv(ENVVAR_TWINE_USERNAME) if not gh_token: logging.fatal( - "Neither environment variables [%s] or [%s] (github token) are set. Aborting." % ( - ENVVAR_GITHUB_TOKEN, ENVVAR_GITHUB_TOKEN2) + "Neither environment variables [%s] or [%s] (github token) are set. Aborting." + % (ENVVAR_GITHUB_TOKEN, ENVVAR_GITHUB_TOKEN2) ) sys.exit(1) if not twine_repo: logging.fatal( - "Environment variables [%s/%s] (PyPI repository/URL) and/or [%s] (PyPI username) are not set. Aborting." % ( + "Environment variables [%s/%s] (PyPI repository/URL) and/or [%s] (PyPI username) are not set. Aborting." + % ( ENVVAR_TWINE_REPOSITORY, ENVVAR_TWINE_REPOSITORY_URL, - ENVVAR_TWINE_USERNAME) + ENVVAR_TWINE_USERNAME, + ) ) sys.exit(1) this_repo = Repo(REPO_ROOT) @@ -168,9 +168,9 @@ def main(): get_latest(this_repo, branch_name) commit_release(this_repo, branch_name, new_version) release_to_pypi(twine_repo, twine_username) - logging.info('Done.') + logging.info("Done.") -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main() diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 4152b89..ced2c10 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -46,8 +46,12 @@ def replace_lines(regexer, handler, lines): result = [] for line in lines: content = line.strip() - replaced = regexer.sub(handler, content) - result.append(line.replace(content, replaced, 1)) + try: + replaced = regexer.sub(handler, content) + except KeyError: + result.append(line) + else: + result.append(line.replace(content, replaced, 1)) return result @@ -101,7 +105,7 @@ def detect_file_triggers(release_commit): """The existence of files matching configured globs will trigger a version bump""" all_valid_trigger_files = set() triggers = set() - for trigger, pattern in config.trigger_patterns.items(): + for pattern, trigger in config.trigger_patterns.items(): matches = glob.glob(pattern) if matches: @@ -182,20 +186,6 @@ def get_lock_behaviour(triggers, all_data, lock): return updates -def get_final_version_string(release_mode, version): - """Generates update dictionary entries for the version string""" - production_version = semver.finalize_version(version) - updates = {} - if release_mode: - updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE - updates[Constants.VERSION_FIELD] = production_version - updates[Constants.VERSION_STRICT_FIELD] = production_version - else: - updates[Constants.VERSION_FIELD] = version - updates[Constants.VERSION_STRICT_FIELD] = production_version - return updates - - def get_dvcs_info(): """Gets current repository info from git""" cmd = "git rev-list --count HEAD" @@ -208,13 +198,15 @@ def get_dvcs_info(): def get_all_versions_from_tags(tags): + """this is like a reverse match from a template""" # build a regex from our version template - re_safe_placeholder = 10 * "v" + re_safe_placeholder = r"A_PLACEHOLDER_FOR_THE_VERSION_DETECTOR" + re_version_detector = r"(\d+\.\d+\.\d+(-\w+.\d+)?(\+\w+.\d+)?)" tag_re = ( "^" + re.escape( config.TAG_TEMPLATE.replace("{version}", re_safe_placeholder) - ).replace(re_safe_placeholder, "(.*)") + ).replace(re_safe_placeholder, re_version_detector) + "$" ) _LOG.debug("regexing with %r", tag_re) @@ -255,8 +247,12 @@ def get_dvcs_commit_for_version(version, persist_from): _LOG.exception("failed to discover the commit for the last tagged release") -def get_dvcs_latest_tag_semver(): - """Gets the semantically latest tag across the whole repo""" +def get_dvcs_ordered_tag_semvers(): + """Gets the semantically latest tag across the whole repo + + :returns: ordered list of VersionInfo instances + :rtype: list(semver.VersionInfo) + """ tag_glob = config.TAG_TEMPLATE.replace("{version}", "*") cmd = "git tag --list %s" % tag_glob tags = str(subprocess.check_output(shlex.split(cmd)).decode("utf8").strip()) @@ -266,20 +262,64 @@ def get_dvcs_latest_tag_semver(): ordered_versions = sorted( {v for v in set(utils.from_text_or_none(version) for version in matches) if v} ) - result = None - if ordered_versions: - result = ordered_versions.pop() - _LOG.info("latest version found in across all dvcs tags: %s", result) - return result + return ordered_versions -def get_dvcs_ancestor_tag_semver(): - """Gets the latest tag that's an ancestor to the current commit""" - cmd = "git describe --abbrev=0 --tags" - version = str(subprocess.check_output(shlex.split(cmd)).decode("utf8").strip()) - result = utils.from_text_or_none(get_all_versions_from_tags([version])[0]) - _LOG.info("latest version found in dvcs nearest tag: %r", result) - return result +def get_dvcs_repo_latest_version_semver(): + """Gets the most recent version across the whole repo""" + ordered_versions = get_dvcs_ordered_tag_semvers() + version = ordered_versions[-1] if ordered_versions else None + _LOG.info("latest version found across all dvcs tags: %s", version) + return version + + +def get_dvcs_repo_latest_release_semver(): + """Gets the most recent release across the whole repo""" + ordered_versions = get_dvcs_ordered_tag_semvers() + for version in reversed(ordered_versions): # type: semver.VersionInfo + if utils.is_release(version): + break + else: + version = None + _LOG.info("latest release found across all dvcs tags: %s", version) + return version + + +def get_dvcs_previous_version_semver(): + """Gets the latest version that's an ancestor to the current commit""" + ordered_versions = get_dvcs_ordered_tag_semvers() + for version in reversed(ordered_versions): # type: semver.VersionInfo + if is_ancestor(version): + break + else: + version = None + _LOG.info("previous version found in ancestral tags: %r", version) + return version + + +def get_dvcs_previous_release_semver(): + """Gets the latest release that's an ancestor to the current commit""" + ordered_versions = get_dvcs_ordered_tag_semvers() + for version in reversed(ordered_versions): # type: semver.VersionInfo + if utils.is_release(version) and is_ancestor(version): + break + else: + version = None + _LOG.info("previous release found in ancestral tags: %r", version) + return version + + +def is_ancestor(version): + try: + # if "--is-ancestor" returns exit code 0, then it is an ancestor and we can stop looking + release_tag = config.TAG_TEMPLATE.replace("{version}", str(version)) + subprocess.check_output( + ["git", "merge-base", "--is-ancestor", release_tag, "HEAD"] + ) + except subprocess.CalledProcessError: + pass + else: + return True def add_dvcs_tag(version): @@ -299,10 +339,14 @@ def get_current_version(persist_from): if source == Constants.FROM_SOURCE: all_data = read_targets(config.targets) version = utils.get_semver_from_source(all_data) - elif source == Constants.FROM_VCS_LATEST: - version = get_dvcs_latest_tag_semver() - elif source == Constants.FROM_VCS_ANCESTOR: - version = get_dvcs_ancestor_tag_semver() + elif source == Constants.FROM_VCS_PREVIOUS_VERSION: + version = get_dvcs_previous_version_semver() + elif source == Constants.FROM_VCS_PREVIOUS_RELEASE: + version = get_dvcs_previous_release_semver() + elif source == Constants.FROM_VCS_LATEST_VERSION: + version = get_dvcs_repo_latest_version_semver() + elif source == Constants.FROM_VCS_LATEST_RELEASE: + version = get_dvcs_repo_latest_release_semver() if version: break return version @@ -318,6 +362,24 @@ def get_overrides(updates, commit_count_as): return overrides +def load_config(config_path): + get_or_create_config(config_path, config) + + for k, v in config.regexers.items(): + config.regexers[k] = re.compile(v) + + # a forward-mapping of the configured aliases + # giving : + # if a value occurs multiple times, we take the last set value + # TODO: the 'forward aliases' things is way overcomplicated + # would be better to rework the config to have keys set-or-None + # since there's only a finite set of valid keys we operate on + config._forward_aliases.clear() + for k, v in config.key_aliases.items(): + config._forward_aliases[v] = k + return config + + def main( set_to=None, commit_count_as=None, @@ -325,6 +387,7 @@ def main( bump=None, lock=None, enable_file_triggers=None, + incr_from_release=None, config_path=None, persist_from=None, persist_to=None, @@ -340,7 +403,7 @@ def main( Write out new version and any other requested variables :param set_to: explicitly set semver to this version string - :param set_patch_count: sets the patch number to the commit count + :param commit_count_as: uses the commit count for the specified sigfig :param release: marks with a production flag just sets a single flag as per config :param bump: string indicating major/minor/patch @@ -349,6 +412,14 @@ def main( lock only removed if a version bump would have occurred :param enable_file_triggers: whether to enable bumping based on file triggers bumping occurs once if any file(s) exist that match the config + :param incr_from_release: dynamically generates the bump by comparing the + proposed triggers for the current version, with the significance of the previous release + to ensure e.g. adding new major changes to a prerelease should probably trigger a new major version + specifically, the bump is: + if (max trigger sigfig) > (max sigfig since release): + (max trigger sigfig) + else + (min trigger sigfig) :param config_path: path to config file :param extra_updates: :return: @@ -356,70 +427,76 @@ def main( updates = {} persist_to = persist_to or [Constants.TO_SOURCE] persist_from = persist_from or [Constants.FROM_SOURCE] - get_or_create_config(config_path, config) - - for k, v in config.regexers.items(): - config.regexers[k] = re.compile(v) - - # a forward-mapping of the configured aliases - # giving : - # if a value occurs multiple times, we take the last set value - # TODO: the 'forward aliases' things is way overcomplicated - # would be better to rework the config to have keys set-or-None - # since there's only a finite set of valid keys we operate on - config._forward_aliases.clear() - for k, v in config.key_aliases.items(): - config._forward_aliases[v] = k + load_config(config_path) all_data = {} + last_release_semver = None + if incr_from_release: + if (Constants.FROM_VCS_PREVIOUS_VERSION in persist_from) or ( + Constants.FROM_VCS_PREVIOUS_RELEASE in persist_from + ): + last_release_semver = get_dvcs_previous_release_semver() + else: + last_release_semver = get_dvcs_repo_latest_release_semver() + _LOG.debug("found previous full release: %s", last_release_semver) current_semver = get_current_version(persist_from) release_commit = get_dvcs_commit_for_version(current_semver, persist_from) - new_semver = current_semver = str(current_semver) triggers = get_all_triggers(bump, enable_file_triggers, release_commit) updates.update(get_lock_behaviour(triggers, all_data, lock)) updates.update(get_dvcs_info()) + new_version = current_semver if set_to: _LOG.debug("setting version directly: %s", set_to) # parse it - validation failure will raise a ValueError - semver.parse(set_to) - new_semver = set_to + new_version = semver.parse_version_info(set_to) if not lock: warnings.warn( "After setting version manually, does it need locking for a CI flow, to avoid an extraneous increment?", UserWarning, ) elif triggers: - # only use triggers if the version is not set directly + # use triggers if the version is not set directly _LOG.debug("auto-incrementing version (triggers: %s)", triggers) overrides = get_overrides(updates, commit_count_as) - new_semver = utils.make_new_semver(current_semver, triggers, **overrides) + new_version = utils.make_new_semver( + current_semver, last_release_semver, triggers, **overrides + ) - updates.update(get_final_version_string(release_mode=release, version=new_semver)) + release_string = semver.finalize_version(str(new_version)) + release_version = semver.parse_version_info(release_string) + if release: + new_version = release_version + updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE + updates[Constants.VERSION_FIELD] = release_string + updates[Constants.VERSION_STRICT_FIELD] = release_string + else: + updates[Constants.VERSION_FIELD] = str(new_version) + updates[Constants.VERSION_STRICT_FIELD] = release_string # write out the individual parts of the version - updates.update(semver.parse(new_semver)) + updates.update(new_version._asdict()) # only rewrite a field that the user has specified in the configuration - native_updates = { + source_file_updates = { native: updates[key] for native, key in config.key_aliases.items() if key in updates } # finally, add in commandline overrides - native_updates.update(extra_updates) + source_file_updates.update(extra_updates) if not dry_run: if Constants.TO_SOURCE in persist_to: - write_targets(config.targets, **native_updates) + write_targets(config.targets, **source_file_updates) if Constants.TO_VCS in persist_to: add_dvcs_tag(updates[Constants.VERSION_FIELD]) else: _LOG.warning("dry run: no changes were made") - return current_semver, new_semver, native_updates + return str(current_semver), str(new_version), source_file_updates def parse_other_args(others): @@ -464,6 +541,7 @@ def main_from_cli(): release=args.release, bump=args.bump, enable_file_triggers=args.file_triggers, + incr_from_release=args.incr_from_release, config_path=args.config, dry_run=args.show, persist_from=args.persist_from, diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 158cc7f..2c4d095 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -32,6 +32,11 @@ def get_cli(): dest="file_triggers", help="Detects need to bump based on presence of files (as specified in config).", ) + parser.add_argument( + "--incr-from-release", + action="store_true", + help="Automatically sets version number based on SCIENCE (see docs). Requires use of VCS tags.", + ) parser.add_argument( "--print-file-triggers", action="store_true", @@ -68,8 +73,10 @@ def get_cli(): "--persist-from", choices={ Constants.FROM_SOURCE, - Constants.FROM_VCS_ANCESTOR, - Constants.FROM_VCS_LATEST, + Constants.FROM_VCS_PREVIOUS_VERSION, + Constants.FROM_VCS_PREVIOUS_RELEASE, + Constants.FROM_VCS_LATEST_VERSION, + Constants.FROM_VCS_LATEST_RELEASE, }, action="append", default=[], diff --git a/src/auto_version/config.py b/src/auto_version/config.py index a6df16d..e7c8175 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -25,8 +25,10 @@ class Constants(object): # source and destination control FROM_SOURCE = "source" - FROM_VCS_ANCESTOR = "vcs" - FROM_VCS_LATEST = "vcs-latest" + FROM_VCS_PREVIOUS_VERSION = "vcs-prev-version" + FROM_VCS_PREVIOUS_RELEASE = "vcs-prev-release" + FROM_VCS_LATEST_VERSION = "vcs-global-version" + FROM_VCS_LATEST_RELEASE = "vcs-global-release" TO_SOURCE = "source" TO_VCS = "vcs" @@ -56,19 +58,24 @@ class AutoVersionConfig(object): targets = [os.path.join("src", "_version.py")] regexers = { ".json": r"""^\s*[\"]?(?P[\w:]+)[\"]?\s*:[\t ]*[\"']?(?P((\\\")?[^\r\n\t\f\v\",](\\\")?)+)[\"']?,?""", # noqa + ".yaml": r"""^\s*[\"']?(?P[\w]+)[\"']?\s*:\s*[\"']?(?P[\w\-.+\\\/:]*[^'\",\[\]#\s]).*""", # noqa + ".yml": r"""^\s*[\"']?(?P[\w]+)[\"']?\s*:\s*[\"']?(?P[\w\-.+\\\/:]*[^'\",\[\]#\s]).*""", # noqa ".py": r"""^\s*['\"]?(?P\w+)['\"]?\s*[=:]\s*['\"]?(?P[^\r\n\t\f\v\"']+)['\"]?,?""", # noqa ".cs": r"""^(\w*\s+)*(?P\w+)\s?[=:]\s*['\"]?(?P[^\r\n\t\f\v\"']+)['\"].*""", # noqa ".csproj": r"""^<(?P\w+)>(?P\S+)<\/\w+>""", # noqa ".properties": r"""^\s*(?P\w+)\s*=[\t ]*(?P[^\r\n\t\f\v\"']+)?""", # noqa } trigger_patterns = { - SemVerSigFig.major: os.path.join("docs", "news", "*.major"), - SemVerSigFig.minor: os.path.join("docs", "news", "*.feature"), - SemVerSigFig.patch: os.path.join("docs", "news", "*.bugfix"), + os.path.join("docs", "news", "*.major"): SemVerSigFig.major, + os.path.join("docs", "news", "*.feature"): SemVerSigFig.minor, + os.path.join("docs", "news", "*.bugfix"): SemVerSigFig.patch, } PRERELEASE_TOKEN = "pre" BUILD_TOKEN = "build" TAG_TEMPLATE = "release/{version}" + MIN_NONE_RELEASE_SIGFIG = ( + "prerelease" + ) # the minimum significant figure to increment is this isn't a release @classmethod def _deflate(cls): diff --git a/src/auto_version/replacement_handler.py b/src/auto_version/replacement_handler.py index a3e5275..d455dca 100644 --- a/src/auto_version/replacement_handler.py +++ b/src/auto_version/replacement_handler.py @@ -17,19 +17,18 @@ def __init__(self, **params): self.missing = set(params.keys()) def __call__(self, match): - """Given a regex Match Object, return the entire replacement string""" + """Given a regex Match Object, return the entire replacement string + + :raises KeyError: + """ original = match.string key = match.group(Constants.KEY_GROUP) - replacement = self.params.get(key) - if replacement is None: # if this isn't a key we are interested in replacing - replaced = original - else: - start, end = match.span(Constants.VALUE_GROUP) - if start < 0: - # when there's a match but zero-length for the value group, we insert it at the end - # of the line just after the last non-whitespace character - # e.g. blah=\n --> blah=text\n - start = end = len(original.rstrip()) - self.missing.remove(key) - replaced = "".join([original[:start], str(replacement), original[end:]]) - return replaced + replacement = self.params[key] # if there's nothing in the lookup, raise KeyError + start, end = match.span(Constants.VALUE_GROUP) + if start < 0: + # when there's a match but zero-length for the value group, we insert it at the end + # of the line just after the last non-whitespace character + # e.g. blah=\n --> blah=text\n + start = end = len(original.rstrip()) + self.missing.remove(key) + return "".join([original[:start], str(replacement), original[end:]]) diff --git a/src/auto_version/tests/example.toml b/src/auto_version/tests/example.toml index 642e704..94f9187 100644 --- a/src/auto_version/tests/example.toml +++ b/src/auto_version/tests/example.toml @@ -12,4 +12,4 @@ RELEASE = "RELEASE_FIELD" [AutoVersionConfig.trigger_patterns] # this will trigger on existence of any python file -minor = "*.py" +"*.py" = "minor" diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 6a6dca1..ab7e70d 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -7,9 +7,12 @@ import subprocess import unittest +import semver import six from auto_version import auto_version_tool +from auto_version import utils from auto_version.auto_version_tool import extract_keypairs +from auto_version.auto_version_tool import get_all_versions_from_tags from auto_version.auto_version_tool import main from auto_version.auto_version_tool import replace_lines from auto_version.config import AutoVersionConfig as config @@ -69,19 +72,21 @@ def test_dev(self): self.assertEqual( updates, { - "VERSION": "19.99.0-dev.1", - "VERSION_AGAIN": "19.99.0-dev.1", - "STRICT_VERSION": "19.99.0", + "VERSION": "19.99.1-dev.1", + "VERSION_AGAIN": "19.99.1-dev.1", + "STRICT_VERSION": "19.99.1", }, ) def test_build(self): + # can't just tag a build onto something that's already a release version + self.call(set_to="19.99.0+build.1") old, new, updates = self.call(bump="build") self.assertEqual( updates, { - "VERSION": "19.99.0+build.1", - "VERSION_AGAIN": "19.99.0+build.1", + "VERSION": "19.99.0+build.2", + "VERSION_AGAIN": "19.99.0+build.2", "STRICT_VERSION": "19.99.0", }, ) @@ -124,7 +129,97 @@ def test_custom_field_set(self): self.assertEqual(updates["UNRELATED_STRING"], "apple") -@unittest.skipIf(os.getenv('CI', False), "Running on CI") +class TestUtils(unittest.TestCase): + def test_is_release(self): + self.assertTrue(utils.is_release(semver.parse_version_info("1.2.3"))) + self.assertFalse(utils.is_release(semver.parse_version_info("1.2.3-RC.1"))) + self.assertFalse(utils.is_release(semver.parse_version_info("1.2.3+abc"))) + + def test_sigfig_max(self): + self.assertEqual("minor", utils.max_sigfig(["minor", "patch"])) + + def test_sigfig_min(self): + self.assertEqual("minor", utils.min_sigfig(["minor", "major"])) + + def test_sigfig_compare_gt(self): + self.assertFalse(utils.sigfig_gt("minor", "major")) + self.assertFalse(utils.sigfig_gt("minor", "minor")) + self.assertTrue(utils.sigfig_gt("major", "patch")) + + def test_sigfig_compare_lt(self): + self.assertTrue(utils.sigfig_lt("minor", "major")) + self.assertFalse(utils.sigfig_lt("minor", "minor")) + self.assertFalse(utils.sigfig_lt("major", "patch")) + + def test_semver_diff(self): + self.assertEqual( + "minor", + utils.semver_diff( + semver.parse_version_info("1.2.3"), semver.parse_version_info("1.3.5") + ), + ) + self.assertEqual( + "patch", + utils.semver_diff( + semver.parse_version_info("1.2.3"), + semver.parse_version_info("1.2.4-RC.1"), + ), + ) + self.assertEqual( + None, + utils.semver_diff( + semver.parse_version_info("1.2.3"), semver.parse_version_info("1.2.3") + ), + ) + + +class TestNewSemVerLogic(unittest.TestCase): + """Unit testing the core logic that determines a bump""" + + @classmethod + def setUpClass(cls): + test_dir = os.path.dirname(__file__) + auto_version_tool.load_config(os.path.join(test_dir, "example.toml")) + + def check(self, previous, current, bumps, expect): + previous = semver.parse_version_info(previous) if previous else None + self.assertEqual( + expect, + str( + utils.make_new_semver( + semver.parse_version_info(current), previous, bumps + ) + ), + ) + + def test_release_bump(self): + self.check(None, "1.2.3", {"minor"}, "1.3.0-dev.1") + + def test_no_history_bump(self): + self.check(None, "1.2.3", {"prerelease"}, "1.2.4-dev.1") + + # this would be wrong, because you can't pre-release something that's released + # self.check(None, "1.2.3", ["prerelease"], "1.2.3-dev.1") + + def test_no_history_pre_bump(self): + self.check(None, "1.2.3-dev.1", {"prerelease"}, "1.2.3-dev.2") + + def test_release_bump_with_history(self): + self.check("1.2.2", "1.2.3", {"minor"}, "1.3.0-dev.1") + + def test_candidate_bump_with_history_less(self): + # the bump is less significant than the original RC increment + self.check("1.0.0", "1.1.0-dev.3", {"patch"}, "1.1.0-dev.4") + + def test_candidate_bump_with_history_same(self): + # the RC has the same significance from the previous release as the bump + self.check("1.2.2", "1.2.3-dev.1", {"patch"}, "1.2.3-dev.2") + + def test_candidate_bump_with_history_more(self): + # the bump is more significant than the previous release, so perform that bump + self.check("1.2.2", "1.2.3-dev.1", {"minor"}, "1.3.0-dev.1") + + class TestVCSTags(unittest.TestCase): call = functools.partial(main, config_path="example.toml") @@ -140,61 +235,140 @@ def tearDownClass(cls): def setUp(self): cmd = "git tag release/4.5.6" subprocess.check_call(shlex.split(cmd)) - - def tearDown(self): - cmd = "git tag --delete release/4.5.6" + cmd = "git tag release/4.5.7-dev.1" subprocess.check_call(shlex.split(cmd)) - try: - cmd = "git tag --delete release/5.0.0-dev.1" - subprocess.check_call(shlex.split(cmd)) - except Exception: - pass - - def test_from_ancestor_tag(self): - """i.e. most immediate ancestor tag""" - bumped = "5.0.0-dev.1" + # todo: build a git tree with a branch, release and RC on that branch + # (to distinguish global vs ancestry tests) + self.addCleanup( + subprocess.check_call, shlex.split("git tag --delete release/4.5.7-dev.1") + ) + self.addCleanup( + subprocess.check_call, shlex.split("git tag --delete release/4.5.6") + ) + + def test_from_ancestor_version(self): + bumped = "4.5.7-dev.1" old, new, updates = self.call( - persist_from=[Constants.FROM_VCS_ANCESTOR], bump="major" + persist_from=[Constants.FROM_VCS_PREVIOUS_VERSION] ) self.assertEqual( updates, - {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, + { + "VERSION": bumped, + "VERSION_AGAIN": bumped, + "STRICT_VERSION": semver.finalize_version(bumped), + }, ) - def test_from_latest_of_all_time(self): - """i.e. latest version tag across the entire repo - (TODO: but we cant test global tags without making a new branch etc etc) - """ - bumped = "5.0.0-dev.1" + def test_from_ancestor_release(self): + bumped = "4.5.6" old, new, updates = self.call( - persist_from=[Constants.FROM_VCS_LATEST], bump="major" + persist_from=[Constants.FROM_VCS_PREVIOUS_RELEASE] ) self.assertEqual( updates, - {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, + { + "VERSION": bumped, + "VERSION_AGAIN": bumped, + "STRICT_VERSION": semver.finalize_version(bumped), + }, + ) + + def test_from_latest_of_all_time(self): + bumped = "4.5.7-dev.1" + old, new, updates = self.call(persist_from=[Constants.FROM_VCS_LATEST_VERSION]) + self.assertEqual( + updates, + { + "VERSION": bumped, + "VERSION_AGAIN": bumped, + "STRICT_VERSION": semver.finalize_version(bumped), + }, + ) + + def test_from_latest_of_all_time_release(self): + bumped = "4.5.6" + old, new, updates = self.call(persist_from=[Constants.FROM_VCS_LATEST_RELEASE]) + self.assertEqual( + updates, + { + "VERSION": bumped, + "VERSION_AGAIN": bumped, + "STRICT_VERSION": semver.finalize_version(bumped), + }, ) def test_to_tag(self): - """writes a tag in git - (TODO: but we cant test global tags without making a new branch etc etc) + """writes a tag in to git """ bumped = "5.0.0-dev.1" old, new, updates = self.call( - persist_from=[Constants.FROM_VCS_LATEST], + persist_from=[Constants.FROM_VCS_LATEST_VERSION], persist_to=[Constants.TO_VCS], bump="major", ) + self.addCleanup( + subprocess.check_call, shlex.split("git tag --delete release/5.0.0-dev.1") + ) self.assertEqual( updates, - {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, + { + "VERSION": bumped, + "VERSION_AGAIN": bumped, + "STRICT_VERSION": semver.finalize_version(bumped), + }, ) - version = auto_version_tool.get_dvcs_latest_tag_semver() + version = auto_version_tool.get_dvcs_repo_latest_version_semver() self.assertEqual( dict(version._asdict()), dict(major=5, minor=0, patch=0, build=None, prerelease="dev.1"), ) +class TestTagReplacements(unittest.TestCase): + some_tags = [ + "0.0.0", + "0.1.0", + "v0.2.0", + "0.3.0v", + "my_project/0.4.0", + "my_project/0.5.0/releases", + "my_project/0.6.0-RC.2+build-99/releases", + r"£*ORWI\H'#[;'Q", + ] + + @classmethod + def setUpClass(cls): + cls._default_template = config.TAG_TEMPLATE + + @classmethod + def tearDownClass(cls): + config.TAG_TEMPLATE = cls._default_template + + def eval(self, template, tags, expect): + config.TAG_TEMPLATE = template + self.assertEqual(get_all_versions_from_tags(tags), expect) + + def test_empty_tag(self): + self.eval("", self.some_tags, []) + + def test_v_tag(self): + self.eval("v{version}", self.some_tags, ["0.2.0"]) + + def test_plain_tag(self): + self.eval("{version}", self.some_tags, ["0.0.0", "0.1.0"]) + + def test_prefix_tag(self): + self.eval("my_project/{version}", self.some_tags, ["0.4.0"]) + + def test_prefix_suffix_tag(self): + self.eval( + "my_project/{version}/releases", + self.some_tags, + ["0.5.0", "0.6.0-RC.2+build-99"], + ) + + @contextlib.contextmanager def Noop(): """A no-op context manager""" @@ -211,18 +385,40 @@ class BaseReplaceCheck(unittest.TestCase): non_matching = [] # specify example lines that should not match def test_match(self): + """ + Check that for each specified line, a match is triggered + + n.b. a match must include the full length of the line, or nothing at all + + if it includes the full length of the line, there must be two named groups + `KEY` and `VALUE` that contain only the key and value respectively + + :return: + """ for line in self.lines: with self.subTest(line=line) if six.PY3 else Noop(): extracted = extract_keypairs([line], self.regexer) self.assertEqual({self.key: self.value}, extracted) def test_non_match(self): + """ + Check lines that shouldn't trigger any matches + :return: + """ for line in self.non_matching: with self.subTest(line=line) if six.PY3 else Noop(): extracted = extract_keypairs([line], self.regexer) self.assertEqual({}, extracted) def test_replace(self): + """ + takes all the 'lines' and generates an expected value with a simple replacement + (1.2.3.4+dev0 -> 5.6.7.8+dev1) + additionally, explicit replacements can be tested + they are all run through the ReplacementHandler to check + the expected value + """ + replacements = {} replacements.update(self.explicit_replacement) replacements.update( @@ -292,3 +488,18 @@ class XMLRegexTest(BaseReplaceCheck): '\r\n', """\r\n""", ] + + +class YamlRegexTest(BaseReplaceCheck): + regexer = re.compile(config.regexers[".yaml"]) + lines = [ + """ "custom_Key": '1.2.3.4+dev0'\r\n""", + """ custom_Key: 1.2.3.4+dev0""", + """ custom_Key: 1.2.3.4+dev0 # comment""", + ] + explicit_replacement = { + " name: python:3.7.1\r\n": " name: python:3.7.1\r\n", + " custom_Key: 1.2.3.4+dev0 # yay": " custom_Key: 5.6.7.8+dev1 # yay", + " CTEST_ARGS: -L node_cpu\r\n": " CTEST_ARGS: -L node_cpu\r\n", + } + non_matching = ["""entrypoint: [""]\r\n"""] # don't match on empty arrays diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index a247a35..3a39d32 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -10,7 +10,10 @@ def from_text_or_none(text): - """A version or None""" + """A version or None + + :rtype: semver.VersionInfo | None + """ if text is not None: try: return semver.parse_version_info(text) @@ -60,7 +63,7 @@ def get_semver_from_source(data): if versions: result = versions[0] _LOG.info("latest version found in source: %r", result) - return result + return semver.parse_version_info(result) def get_token_args(sig_fig): @@ -72,41 +75,90 @@ def get_token_args(sig_fig): return token_args -def make_new_semver(version_string, all_triggers, **overrides): +def max_sigfig(sigfigs): + """Given a list of significant figures, return the largest""" + for sig_fig in SemVerSigFig: # iterate sig figs in order of significance + if sig_fig in sigfigs: + return sig_fig + + +def min_sigfig(sigfigs): + """Given a list of significant figures, return the smallest""" + for sig_fig in reversed(SemVerSigFig): # iterate sig figs in order of least significance + if sig_fig in sigfigs: + return sig_fig + + +def semver_diff(semver1, semver2): + """Given some semvers, return the largest difference between them""" + for sig_fig in SemVerSigFig: + if getattr(semver1, sig_fig) != getattr(semver2, sig_fig): + return sig_fig + + +def sigfig_gt(sig_fig1, sig_fig2): + """Returns True if sf1 > sf2""" + return SemVerSigFig.index(sig_fig1) < SemVerSigFig.index(sig_fig2) + + +def sigfig_lt(sig_fig1, sig_fig2): + """Returns True if sf1 < sf2""" + return SemVerSigFig.index(sig_fig1) > SemVerSigFig.index(sig_fig2) + + +def is_release(semver): + """is a semver a release version""" + return not (semver.build or semver.prerelease) + + +def make_new_semver(current_semver, last_release_semver, all_triggers, **overrides): """Defines how to increment semver based on which significant figure is triggered - (most significant takes precendence) - :param version_string: the version to increment - :param all_triggers: major/minor/patch/prerelease + :param current_semver: the version to increment + :param last_release_semver: the previous release version, if available + :param all_triggers: list of major/minor/patch/prerelease :param overrides: explicit values for some or all of the sigfigs :return: """ + version_string = str(current_semver) + + # if the current version isn't a full release + if not is_release(current_semver) and last_release_semver: + # we check to see how important the changes are + # in the triggers, compared to the changes made between the current version and previous release + if sigfig_gt(max_sigfig(all_triggers), semver_diff(current_semver, last_release_semver)): + # here, the changes are more significant than the original RC bump, so we re-bump + pass + else: + # here the changes are same or lesser than the original RC bump, so we only bump prerelease + all_triggers = {SemVerSigFig.prerelease} - # perform an increment using the most-significant trigger - also_prerelease = True - for sig_fig in SemVerSigFig: # iterate sig figs in order of significance - if sig_fig in all_triggers: - if sig_fig in (SemVerSigFig.prerelease, SemVerSigFig.build): - also_prerelease = False - version_string = getattr(semver, "bump_" + sig_fig)( - version_string, **get_token_args(sig_fig) - ) - break - - if also_prerelease: - # if we *didnt* increment sub-patch, then we should do so - # this provides the "devmode template" as previously - # and ensures a simple 'bump' doesn't look like a full release - version_string = semver.bump_prerelease( - version_string, token=config.PRERELEASE_TOKEN + if is_release(current_semver): + # if the current semver is a release, we can't just do a prerelease or build increment + # there *must* be a minimum patch increment, otherwise you could get 2.0.0 -> 2.0.0-RC.1 + all_triggers.add(SemVerSigFig.patch) + + bump_sigfig = max_sigfig(all_triggers) + + if bump_sigfig: + # perform an increment using the most-significant trigger + version_string = getattr(semver, "bump_" + bump_sigfig)( + str(current_semver), **get_token_args(bump_sigfig) ) - # perform any explicit setting of parts + if sigfig_gt(bump_sigfig, SemVerSigFig.prerelease): + # if we *didnt* increment sub-patch already, then we should do so + # this provides the "devmode template" as previously + # and ensures a simple 'bump' doesn't look like a full release + version_string = semver.bump_prerelease( + version_string, token=config.PRERELEASE_TOKEN + ) + + # perform any explicit setting of sigfigs version_info = semver.parse_version_info(version_string) for k, v in overrides.items(): token_args = get_token_args(k) prefix = list(token_args.values()).pop() + "." if token_args else "" setattr(version_info, "_" + k, prefix + str(v)) - version_string = str(version_info) - return version_string + return version_info From c510bed844bb936f99582e40e60413340e5876ab Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Fri, 5 Feb 2021 01:03:21 +0000 Subject: [PATCH 13/26] Create 10.doc --- docs/news/10.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/news/10.doc diff --git a/docs/news/10.doc b/docs/news/10.doc new file mode 100644 index 0000000..61e42ad --- /dev/null +++ b/docs/news/10.doc @@ -0,0 +1 @@ +Releasing new version From 52c6ff757ae0a966dc8f99379c97d1831f62224a Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Wed, 28 Apr 2021 11:01:06 +0100 Subject: [PATCH 14/26] Fix dependency version (#60) * Update requirements.txt * Create 12.bugfix --- docs/news/12.bugfix | 1 + requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/news/12.bugfix diff --git a/docs/news/12.bugfix b/docs/news/12.bugfix new file mode 100644 index 0000000..99e62e1 --- /dev/null +++ b/docs/news/12.bugfix @@ -0,0 +1 @@ +Fix SemVer dependency version diff --git a/requirements.txt b/requirements.txt index b7c9825..d57f151 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ toml -semver +semver~=2.10 From 7ed03d4dce3825c9aac4a21a24106fabb8812df5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 16:45:09 +0100 Subject: [PATCH 15/26] Upgrade to GitHub-native Dependabot (#61) Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..71041c1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: daily + timezone: Europe/London + open-pull-requests-limit: 1 + ignore: + - dependency-name: twine + versions: + - 3.3.0 + - 3.4.0 + rebase-strategy: disabled From 8712f0242e556d4b94fd0133b38e3fb58dd1404c Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Tue, 18 May 2021 16:53:54 +0100 Subject: [PATCH 16/26] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ab3ef1e..dbe558a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Cross-language tool written in Python to automatically version projects using [S [![CircleCI](https://circleci.com/gh/ARMmbed/autoversion.svg?style=svg&circle-token=dd9ec017be37f9b5f0a5b9a785c55c53fcd578c7)](https://circleci.com/gh/ARMmbed/autoversion) + [![PyPI version](https://badge.fury.io/py/pyautoversion.svg)](https://badge.fury.io/py/pyautoversion) From fe0ff9f57fc173e91fbc31f0d70ab52360f21cba Mon Sep 17 00:00:00 2001 From: monty bot Date: Tue, 18 May 2021 16:35:47 +0000 Subject: [PATCH 17/26] :checkered_flag: :newspaper: Releasing version 2.0.0 @ 2021-05-18 16:35 [ci skip] --- CHANGELOG.md | 17 +++++++++++++++++ docs/news/10.doc | 1 - docs/news/12.bugfix | 1 - docs/news/5.feature | 1 - docs/news/6.feature | 2 -- src/auto_version/__version__.py | 4 ++-- 6 files changed, 19 insertions(+), 7 deletions(-) delete mode 100644 docs/news/10.doc delete mode 100644 docs/news/12.bugfix delete mode 100644 docs/news/5.feature delete mode 100644 docs/news/6.feature diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f8d1e..c5d5718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ a list of versions that have been released on PyPI. [//]: # (begin_release_notes) +2.0.0 (2021-05-18) +### Features + +- Adds better workflow for incrementing patches. (#5) + +- Adds the ability to have "infinite" newsfiles. They no longer require cleaning up, if tags are used to indicate releases. + This requires a workflow where releases are tagged in git, so we can determine the "new news". (#6) + +### Bugfixes + +- Fix SemVer dependency version (#12) + +### Improved Documentation + +- Releasing new version (#10) + + 1.2.0 (2019-11-15) ================== diff --git a/docs/news/10.doc b/docs/news/10.doc deleted file mode 100644 index 61e42ad..0000000 --- a/docs/news/10.doc +++ /dev/null @@ -1 +0,0 @@ -Releasing new version diff --git a/docs/news/12.bugfix b/docs/news/12.bugfix deleted file mode 100644 index 99e62e1..0000000 --- a/docs/news/12.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix SemVer dependency version diff --git a/docs/news/5.feature b/docs/news/5.feature deleted file mode 100644 index 49d362a..0000000 --- a/docs/news/5.feature +++ /dev/null @@ -1 +0,0 @@ -Adds better workflow for incrementing patches. \ No newline at end of file diff --git a/docs/news/6.feature b/docs/news/6.feature deleted file mode 100644 index b7ae49b..0000000 --- a/docs/news/6.feature +++ /dev/null @@ -1,2 +0,0 @@ -Adds the ability to have "infinite" newsfiles. They no longer require cleaning up, if tags are used to indicate releases. - This requires a workflow where releases are tagged in git, so we can determine the "new news". \ No newline at end of file diff --git a/src/auto_version/__version__.py b/src/auto_version/__version__.py index bf8c320..2998a0d 100644 --- a/src/auto_version/__version__.py +++ b/src/auto_version/__version__.py @@ -1,4 +1,4 @@ # This project's release version -__version__ = "1.2.0" +__version__ = "2.0.0" # This project's release commit hash -COMMIT = "3d680ed58eea0930e4fa172b8c0261566a8dc3b9" +COMMIT = "8712f0242e556d4b94fd0133b38e3fb58dd1404c" From d217e70d34a6741d3f28e16fd063919f01a0dbdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 May 2021 18:16:01 +0100 Subject: [PATCH 18/26] Update semver requirement from ~=2.10 to ~=2.13 (#62) Updates the requirements on [semver](https://github.com/python-semver/python-semver) to permit the latest version. - [Release notes](https://github.com/python-semver/python-semver/releases) - [Changelog](https://github.com/python-semver/python-semver/blob/master/docs/changelog-semver2.rst) - [Commits](https://github.com/python-semver/python-semver/compare/2.10.0...2.13.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Adrien CABARBAYE --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d57f151..1bb302b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ toml -semver~=2.10 +semver~=2.13 From a8db19e3e11691f3e38d06977ad2d8157dc0c0d2 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Tue, 18 May 2021 18:20:56 +0100 Subject: [PATCH 19/26] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d5718..7a5063e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ a list of versions that have been released on PyPI. [//]: # (begin_release_notes) 2.0.0 (2021-05-18) +================== + ### Features - Adds better workflow for incrementing patches. (#5) From fc8dce7227c54931b134d54481a1c2a8fdd46ea5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Aug 2021 19:56:33 +0100 Subject: [PATCH 20/26] Bump pytest from 5.4.1 to 6.2.4 (#63) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.1 to 6.2.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.4.1...6.2.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 6d17e89..29f1ae9 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,7 @@ six = "*" coverage = "*" # locked to protect from Py2/3 compatibility break in more recent versions towncrier = "*" -pytest = "==5.4.1" +pytest = "==6.2.4" pytest-cov = "==2.8.1" pytest-html = "==2.1.1" pyautoversion = {path = ".",editable = true} From 10167be67bdcd8bbde829f6cc9d6206911bd008e Mon Sep 17 00:00:00 2001 From: Alexey Filippov Date: Thu, 5 Aug 2021 20:05:01 +0100 Subject: [PATCH 21/26] Avoid KeyError on variable reuse (#64) Co-authored-by: Adrien CABARBAYE --- requirements.txt | 1 + src/auto_version/replacement_handler.py | 3 +- src/auto_version/tests/double_target.toml | 15 ++++++++++ src/auto_version/tests/example2.py | 6 ++++ src/auto_version/tests/test_autoversion.py | 33 ++++++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/auto_version/tests/double_target.toml create mode 100644 src/auto_version/tests/example2.py diff --git a/requirements.txt b/requirements.txt index 1bb302b..1dc4903 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ toml semver~=2.13 +six diff --git a/src/auto_version/replacement_handler.py b/src/auto_version/replacement_handler.py index d455dca..5cba1cd 100644 --- a/src/auto_version/replacement_handler.py +++ b/src/auto_version/replacement_handler.py @@ -30,5 +30,6 @@ def __call__(self, match): # of the line just after the last non-whitespace character # e.g. blah=\n --> blah=text\n start = end = len(original.rstrip()) - self.missing.remove(key) + if key in self.missing: + self.missing.remove(key) return "".join([original[:start], str(replacement), original[end:]]) diff --git a/src/auto_version/tests/double_target.toml b/src/auto_version/tests/double_target.toml new file mode 100644 index 0000000..2b374f7 --- /dev/null +++ b/src/auto_version/tests/double_target.toml @@ -0,0 +1,15 @@ +[AutoVersionConfig] +CONFIG_NAME = 'example' +PRERELEASE_TOKEN = 'dev' +targets = ["example.py", "example2.py"] + +[AutoVersionConfig.key_aliases] +VERSION = "VERSION_KEY" +VERSION_AGAIN = "VERSION_KEY" +STRICT_VERSION = "VERSION_KEY_STRICT" +LOCK = "VERSION_LOCK" +RELEASE = "RELEASE_FIELD" + +[AutoVersionConfig.trigger_patterns] +# this will trigger on existence of any python file +"*.py" = "minor" diff --git a/src/auto_version/tests/example2.py b/src/auto_version/tests/example2.py new file mode 100644 index 0000000..f5e9279 --- /dev/null +++ b/src/auto_version/tests/example2.py @@ -0,0 +1,6 @@ +LOCK = False +RELEASE = True +VERSION = "19.99.0" +VERSION_AGAIN = "19.99.0" +STRICT_VERSION = "19.99.0" +UNRELATED_STRING = "apple" diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index ab7e70d..b9bec47 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -129,6 +129,39 @@ def test_custom_field_set(self): self.assertEqual(updates["UNRELATED_STRING"], "apple") +class TestMultiFileBumps(unittest.TestCase): + call = functools.partial(main, config_path="double_target.toml") + + @classmethod + def setUpClass(cls): + dir = os.path.dirname(__file__) + os.chdir(os.path.abspath(dir)) + + def tearDown(self): + self.call(set_to="19.99.0") + + def test_bump_patch(self): + old, new, updates = self.call(bump="patch", release=True) + self.assertEqual( + updates, + { + "RELEASE": True, + "VERSION": "19.99.1", + "VERSION_AGAIN": "19.99.1", + "STRICT_VERSION": "19.99.1", + }, + ) + with open("example2.py", "r") as f: + second_file = f.read() + self.assertEqual(second_file, '''LOCK = False +RELEASE = True +VERSION = "19.99.1" +VERSION_AGAIN = "19.99.1" +STRICT_VERSION = "19.99.1" +UNRELATED_STRING = "apple" +''') + + class TestUtils(unittest.TestCase): def test_is_release(self): self.assertTrue(utils.is_release(semver.parse_version_info("1.2.3"))) From 9e8f2f1311db307ffa80c7658f90bdf2b5eab7e4 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 5 Aug 2021 20:06:52 +0100 Subject: [PATCH 22/26] Create 1.bugfix --- docs/news/1.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/news/1.bugfix diff --git a/docs/news/1.bugfix b/docs/news/1.bugfix new file mode 100644 index 0000000..c445f42 --- /dev/null +++ b/docs/news/1.bugfix @@ -0,0 +1 @@ +Fix bug when having multiple file targets From 8b513e5c57373b84fe66e0b4432aa8471d374bf3 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 5 Aug 2021 20:07:16 +0100 Subject: [PATCH 23/26] Delete 3.major --- docs/news/3.major | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 docs/news/3.major diff --git a/docs/news/3.major b/docs/news/3.major deleted file mode 100644 index 359682d..0000000 --- a/docs/news/3.major +++ /dev/null @@ -1,2 +0,0 @@ -Breaking change to the config spec: the trigger_patterns are specified as pattern:sigfig -in order to support multiple file patterns for each significant figure From 21c7c311e0cf7c178617d956ca9a0973ea436537 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 5 Aug 2021 20:07:27 +0100 Subject: [PATCH 24/26] Delete 4.major --- docs/news/4.major | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 docs/news/4.major diff --git a/docs/news/4.major b/docs/news/4.major deleted file mode 100644 index 48e5826..0000000 --- a/docs/news/4.major +++ /dev/null @@ -1,2 +0,0 @@ -Reworked the DVCS persistence to load from previous tags either globally, or previous to the current commit. -The CLI `--persist-from` options were renamed to accommodate this. \ No newline at end of file From e542c61ad254274aaede0eba2f9b3c405c71637c Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 5 Aug 2021 20:07:37 +0100 Subject: [PATCH 25/26] Delete 7.major --- docs/news/7.major | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 docs/news/7.major diff --git a/docs/news/7.major b/docs/news/7.major deleted file mode 100644 index 8482ce8..0000000 --- a/docs/news/7.major +++ /dev/null @@ -1,2 +0,0 @@ -Breaking change to the config spec: the trigger_patterns are specified as pattern:sigfig -in order to support multiple file patterns for each significant figure \ No newline at end of file From 0c84d38f7341482cc9cadf5ec0e8cdcf33b47e6e Mon Sep 17 00:00:00 2001 From: monty bot Date: Thu, 5 Aug 2021 19:16:56 +0000 Subject: [PATCH 26/26] :checkered_flag: :newspaper: Releasing version 2.0.1 @ 2021-08-05 19:16 [ci skip] --- CHANGELOG.md | 6 ++++++ docs/news/1.bugfix | 1 - src/auto_version/__version__.py | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) delete mode 100644 docs/news/1.bugfix diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5063e..3f74060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ a list of versions that have been released on PyPI. [//]: # (begin_release_notes) +2.0.1 (2021-08-05) +### Bugfixes + +- Fix bug when having multiple file targets (#1) + + 2.0.0 (2021-05-18) ================== diff --git a/docs/news/1.bugfix b/docs/news/1.bugfix deleted file mode 100644 index c445f42..0000000 --- a/docs/news/1.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug when having multiple file targets diff --git a/src/auto_version/__version__.py b/src/auto_version/__version__.py index 2998a0d..4e5d9e4 100644 --- a/src/auto_version/__version__.py +++ b/src/auto_version/__version__.py @@ -1,4 +1,4 @@ # This project's release version -__version__ = "2.0.0" +__version__ = "2.0.1" # This project's release commit hash -COMMIT = "8712f0242e556d4b94fd0133b38e3fb58dd1404c" +COMMIT = "e542c61ad254274aaede0eba2f9b3c405c71637c"