From c8a025ccd0de1cf7cb14b27cb2f8d14f18995055 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 26 Feb 2019 12:52:46 +0000 Subject: [PATCH 01/28] add a failing test for prerelease increment --- src/auto_version/tests/test_autoversion.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index f558fd4..ce96fa7 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -54,6 +54,12 @@ def test_dev(self): updates, {"VERSION": "19.99.0.devX", "VERSION_AGAIN": "19.99.0.devX"} ) + def test_increment_prerelease(self): + old, new, updates = self.call(set_to="1.2.3-RC.1") + self.assertEqual(new, "1.2.3-RC.1") + old, new, updates = self.call(bump="prerelease") + self.assertEqual(new, "1.2.3-RC.2") + def test_end_to_end(self): self.call(bump="major") filepath = os.path.join(os.path.dirname(__file__), "example.py") From e53ec58ae6f8365f69fe83e10aea251d52de7326 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 26 Feb 2019 13:14:32 +0000 Subject: [PATCH 02/28] add precommit definition --- .pre-commit-config.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e497008 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +fail_fast: false +repos: +- repo: https://github.com/ambv/black + rev: 18.9b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.1.0 + hooks: + - id: check-executables-have-shebangs + - id: check-yaml + - id: check-ast + - id: flake8 +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.4 + hooks: + - id: isort From 866a728ed40679eada47fa1f2e779a72f5595231 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 26 Feb 2019 13:15:44 +0000 Subject: [PATCH 03/28] add precommit hooks file --- .pre-commit-config.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e497008 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +fail_fast: false +repos: +- repo: https://github.com/ambv/black + rev: 18.9b0 + hooks: + - id: black +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.1.0 + hooks: + - id: check-executables-have-shebangs + - id: check-yaml + - id: check-ast + - id: flake8 +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.4 + hooks: + - id: isort From bb03fd1f4c4c7dc22593d65bfc96e419d09c166b Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 27 Feb 2019 12:10:48 +0000 Subject: [PATCH 04/28] move to semver library to support incrementing prerelease and build flags --- Pipfile | 1 + Pipfile.lock | 80 ++++++++------ setup.cfg | 4 + src/auto_version/__version__.py | 4 +- src/auto_version/auto_version_tool.py | 121 ++++++++++++--------- src/auto_version/cli.py | 17 +-- src/auto_version/config.py | 8 +- src/auto_version/definitions.py | 2 +- src/auto_version/semver.py | 78 ------------- src/auto_version/tests/example.py | 5 +- src/auto_version/tests/example.toml | 3 +- src/auto_version/tests/test_autoversion.py | 115 +++++++++++++++----- src/auto_version/utils.py | 103 ++++++++++++++++++ 13 files changed, 328 insertions(+), 213 deletions(-) delete mode 100644 src/auto_version/semver.py create mode 100644 src/auto_version/utils.py diff --git a/Pipfile b/Pipfile index 6f93db8..72d2fb8 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] # defer to setup.py pyautoversion = {path = ".",editable = true} +semver = "*" [dev-packages] "flake8" = "*" diff --git a/Pipfile.lock b/Pipfile.lock index e50f3f8..d5eeedb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8a3d9a48b3bada7f18627e576a87c2004f5e50ccd14558e461596929dce9c3d9" + "sha256": "54ba7c29cdda99d528d8eadb42b754a1461c09502b80b1e822a297f2efa285b8" }, "pipfile-spec": 6, "requires": { @@ -20,6 +20,14 @@ "editable": true, "path": "." }, + "semver": { + "hashes": [ + "sha256:41c9aa26c67dc16c54be13074c352ab666bce1fa219c7110e8f03374cd4206b0", + "sha256:5b09010a66d9a3837211bb7ae5a20d10ba88f8cb49e92cb139a69ef90d5060d8" + ], + "index": "pypi", + "version": "==2.8.1" + }, "toml": { "hashes": [ "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", @@ -116,11 +124,11 @@ }, "flake8": { "hashes": [ - "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", - "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.7.6" + "version": "==3.7.7" }, "green": { "hashes": [ @@ -145,36 +153,36 @@ }, "markupsafe": { "hashes": [ - "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", - "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", - "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", - "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", - "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", - "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", - "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", - "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", - "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", - "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", - "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", - "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", - "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", - "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", - "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", - "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", - "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", - "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", - "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", - "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", - "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", - "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", - "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", - "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", - "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", - "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", - "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", - "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" ], - "version": "==1.1.0" + "version": "==1.1.1" }, "mccabe": { "hashes": [ @@ -200,10 +208,10 @@ }, "py": { "hashes": [ - "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", - "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" ], - "version": "==1.7.0" + "version": "==1.8.0" }, "pyautoversion": { "editable": true, diff --git a/setup.cfg b/setup.cfg index 0a52fad..a997387 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,10 @@ exclude = max-complexity = 10 max-line-length = 120 +[isort] +max-line-length=110 +force_single_line=True + [coverage:run] branch = true parallel = true diff --git a/src/auto_version/__version__.py b/src/auto_version/__version__.py index 14939cc..070134f 100644 --- a/src/auto_version/__version__.py +++ b/src/auto_version/__version__.py @@ -1,4 +1,4 @@ # This project's release version -__version__ = "1.0.24.dev54" +__version__ = "1.1.0-pre.67+build.60" # This project's release commit hash -COMMIT = "c5b127d2059c8219f5637fe45bf9e1be3a0af2aa" +COMMIT = "e53ec58ae6f8365f69fe83e10aea251d52de7326" diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 10a5762..f7e2bba 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -21,16 +21,16 @@ import subprocess import warnings +import semver +from auto_version import __version__ +from auto_version import definitions +from auto_version import utils from auto_version.cli import get_cli from auto_version.config import AutoVersionConfig as config from auto_version.config import Constants from auto_version.config import get_or_create_config -import auto_version.definitions from auto_version.replacement_handler import ReplacementHandler -from auto_version import semver -from auto_version import __version__ - _LOG = logging.getLogger(__file__) @@ -136,23 +136,17 @@ def get_lock_behaviour(triggers, all_data, lock): return updates -def get_final_version_string(release_mode, semver, commit_count=0): +def get_final_version_string(release_mode, version): """Generates update dictionary entries for the version string""" - version_string = ".".join(semver) - maybe_dev_version_string = version_string + production_version = semver.finalize_version(version) updates = {} if release_mode: - # in production, we have something like `1.2.3`, as well as a flag e.g. PRODUCTION=True updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE + updates[Constants.VERSION_FIELD] = production_version + updates[Constants.VERSION_STRICT_FIELD] = production_version else: - # in dev mode, we have a dev marker e.g. `1.2.3.dev678` - maybe_dev_version_string = config.DEVMODE_TEMPLATE.format( - version=version_string, count=commit_count - ) - - # make available all components of the semantic version including the full string - updates[Constants.VERSION_FIELD] = maybe_dev_version_string - updates[Constants.VERSION_STRICT_FIELD] = version_string + updates[Constants.VERSION_FIELD] = version + updates[Constants.VERSION_STRICT_FIELD] = production_version return updates @@ -169,8 +163,14 @@ def get_dvcs_info(): def get_all_versions_from_tags(tags): # build a regex from our version template - # (you're ok as long as you don't have our secret string in your template ...) - tag_re = "^" + re.escape(config.TAG_TEMPLATE.replace("{version}", "vvvvv")).replace("vvvvv", "(.*)") + "$" + re_safe_placeholder = 10 * "v" + tag_re = ( + "^" + + re.escape( + config.TAG_TEMPLATE.replace("{version}", re_safe_placeholder) + ).replace(re_safe_placeholder, "(.*)") + + "$" + ) _LOG.debug("regexing with %r", tag_re) tag_re_comp = re.compile(tag_re) matches = [] @@ -192,7 +192,9 @@ def get_dvcs_latest_tag_semver(): tags = tags.splitlines() _LOG.debug("all tags matching simple pattern %r : %s", tag_glob, tags) matches = get_all_versions_from_tags(tags) - ordered_versions = sorted(set(semver.from_text(version) for version in matches)) + ordered_versions = sorted( + set(utils.from_text_or_none(version) for version in matches) + ) _LOG.debug("matched tag versions %s", ordered_versions) return ordered_versions.pop() @@ -201,19 +203,42 @@ 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()) - return semver.from_text(get_all_versions_from_tags([version])[0]) + return utils.from_text_or_none(get_all_versions_from_tags([version])[0]) def add_dvcs_tag(version): """Sets a tag on the current commit""" - cmd = 'git tag -a %s -m "version %s"' % (config.TAG_TEMPLATE.format(version=version), version) + cmd = 'git tag -a %s -m "version %s"' % ( + config.TAG_TEMPLATE.format(version=version), + version, + ) version = str(subprocess.check_output(shlex.split(cmd)).decode("utf8").strip()) return version +def get_current_version(persist_from): + if persist_from == Constants.FROM_SOURCE: + all_data = read_targets(config.targets) + return utils.get_semver_from_source(all_data) + elif persist_from == Constants.FROM_VCS_LATEST: + return get_dvcs_latest_tag_semver() + elif persist_from == Constants.FROM_VCS_ANCESTOR: + return get_dvcs_ancestor_tag_semver() + + +def get_overrides(updates, commit_count_as): + overrides = {} + if commit_count_as: + _ = definitions.SemVerSigFig._asdict()[commit_count_as] + commit_number = updates[Constants.COMMIT_COUNT_FIELD] + _LOG.debug("using commit count for %s: %s", commit_count_as, commit_number) + overrides[commit_count_as] = commit_number + return overrides + + def main( set_to=None, - set_patch_count=None, + commit_count_as=None, release=None, bump=None, lock=None, @@ -252,6 +277,10 @@ def main( if config_path: get_or_create_config(config_path, config) + # perform some input validation + if bump: + _ = definitions.SemVerSigFig._asdict()[bump] + for k, v in config.regexers.items(): config.regexers[k] = re.compile(v) @@ -262,48 +291,32 @@ def main( config._forward_aliases[v] = k all_data = {} - if persist_from == Constants.FROM_SOURCE: - all_data = read_targets(config.targets) - current_semver = semver.get_current_semver(all_data) - elif persist_from == Constants.FROM_VCS_LATEST: - current_semver = get_dvcs_latest_tag_semver() - elif persist_from == Constants.FROM_VCS_ANCESTOR: - current_semver = get_dvcs_ancestor_tag_semver() - + current_semver = get_current_version(persist_from) + new_semver = current_semver = str(current_semver) triggers = get_all_triggers(bump, file_triggers) updates.update(get_lock_behaviour(triggers, all_data, lock)) updates.update(get_dvcs_info()) if set_to: _LOG.debug("setting version directly: %s", set_to) - new_semver = auto_version.definitions.SemVer(*set_to.split(".")) + # parse it - validation failure will raise a ValueError + semver.parse(set_to) + new_semver = set_to if not lock: warnings.warn( - "After setting version manually, does it need locking for a CI flow?", + "After setting version manually, does it need locking for a CI flow, to avoid an extraneous increment?", UserWarning, ) - elif set_patch_count: - _LOG.debug( - "auto-incrementing version, using commit count for patch: %s", - updates[Constants.COMMIT_COUNT_FIELD], - ) - new_semver = semver.make_new_semver( - current_semver, triggers, patch=updates[Constants.COMMIT_COUNT_FIELD] - ) - else: - _LOG.debug("auto-incrementing version") - new_semver = semver.make_new_semver(current_semver, triggers) - - updates.update( - get_final_version_string( - release_mode=release, - semver=new_semver, - commit_count=updates.get(Constants.COMMIT_COUNT_FIELD, 0), - ) - ) + elif triggers: + # only 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) + + updates.update(get_final_version_string(release_mode=release, version=new_semver)) - for part in semver.SemVerSigFig: - updates[part] = getattr(new_semver, part) + # write out the individual parts of the version + updates.update(semver.parse(new_semver)) # only rewrite a field that the user has specified in the configuration native_updates = { @@ -361,7 +374,7 @@ def main_from_cli(): old, new, updates = main( set_to=args.set, - set_patch_count=args.set_patch_count, + commit_count_as=args.commit_count_as, lock=args.lock, release=args.release, bump=args.bump, diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 2a594b5..8e27fe2 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -2,10 +2,9 @@ import argparse import os -from auto_version.config import AutoVersionConfig as config +from auto_version import __version__ from auto_version.config import Constants from auto_version.definitions import SemVerSigFig -from auto_version import __version__ def get_cli(): @@ -32,9 +31,9 @@ def get_cli(): help="Set the SemVer string. Use this locally to set the project version explicitly.", ) parser.add_argument( - "--set-patch-count", - action="store_true", - help="Sets the patch number to the commit count.", + "--commit-count-as", + choices=SemVerSigFig, + help="Use the commit count to set the value of the specified field.", ) parser.add_argument( "--lock", @@ -56,7 +55,11 @@ def get_cli(): ) parser.add_argument( "--persist-from", - choices={Constants.FROM_SOURCE, Constants.FROM_VCS_ANCESTOR, Constants.FROM_VCS_LATEST}, + choices={ + Constants.FROM_SOURCE, + Constants.FROM_VCS_ANCESTOR, + Constants.FROM_VCS_LATEST, + }, default=Constants.FROM_SOURCE, help="Where the current version is stored. This is the version that will be incremented.", ) @@ -71,7 +74,7 @@ def get_cli(): parser.add_argument( "--config", help="Configuration file path. (default: %s)." % default_config_file_path, - default=default_config_file_path + default=default_config_file_path, ) parser.add_argument( "-v", diff --git a/src/auto_version/config.py b/src/auto_version/config.py index da52070..a6df16d 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -3,7 +3,6 @@ import os import toml - from auto_version.definitions import SemVerSigFig _LOG = logging.getLogger(__name__) @@ -54,9 +53,7 @@ class AutoVersionConfig(object): Constants.COMMIT_FIELD: Constants.COMMIT_FIELD, } _forward_aliases = {} # autopopulated later - reverse mapping of the above - targets = [ - os.path.join("src", "_version.py"), - ] + targets = [os.path.join("src", "_version.py")] regexers = { ".json": r"""^\s*[\"]?(?P[\w:]+)[\"]?\s*:[\t ]*[\"']?(?P((\\\")?[^\r\n\t\f\v\",](\\\")?)+)[\"']?,?""", # noqa ".py": r"""^\s*['\"]?(?P\w+)['\"]?\s*[=:]\s*['\"]?(?P[^\r\n\t\f\v\"']+)['\"]?,?""", # noqa @@ -69,7 +66,8 @@ class AutoVersionConfig(object): SemVerSigFig.minor: os.path.join("docs", "news", "*.feature"), SemVerSigFig.patch: os.path.join("docs", "news", "*.bugfix"), } - DEVMODE_TEMPLATE = "{version}.dev{count}" + PRERELEASE_TOKEN = "pre" + BUILD_TOKEN = "build" TAG_TEMPLATE = "release/{version}" @classmethod diff --git a/src/auto_version/definitions.py b/src/auto_version/definitions.py index 1615a25..deb3cf6 100644 --- a/src/auto_version/definitions.py +++ b/src/auto_version/definitions.py @@ -1,5 +1,5 @@ """Shorthand definitions for SemVer objects""" from collections import namedtuple -SemVer = namedtuple("SemVerFields", ["major", "minor", "patch"]) +SemVer = namedtuple("SemVerFields", ["major", "minor", "patch", "prerelease", "build"]) SemVerSigFig = SemVer(*SemVer._fields) diff --git a/src/auto_version/semver.py b/src/auto_version/semver.py deleted file mode 100644 index d7f7164..0000000 --- a/src/auto_version/semver.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Functions for manipulating SemVer objects (Major.Minor.Patch)""" -import logging -import re - -from auto_version.config import AutoVersionConfig as config -from auto_version.config import Constants -from auto_version.definitions import SemVer -from auto_version.definitions import SemVerSigFig - -_LOG = logging.getLogger(__file__) - -re_semver = re.compile(r"""(?P\d+).(?P\d+).(?P\d+)(?P.*)""") - - -def from_text(text): - """A version or None""" - match = re_semver.match(text) - if match: - parts = match.groupdict() - parts.pop("tail") - return SemVer(**parts) - - -def get_current_semver(data): - """Given a dictionary of all version data available, determine the current version""" - # get the not-none values from data - known = { - key: data.get(alias) - for key, alias in config._forward_aliases.items() - if data.get(alias) is not None - } - - # prefer the strict field, if available - potentials = [ - known.pop(Constants.VERSION_STRICT_FIELD, None), - known.pop(Constants.VERSION_FIELD, None), - ] - - from_components = [known.get(k) for k in SemVerSigFig._fields if k in known] - if len(from_components) == 3: - potentials.append(".".join(from_components)) - - versions = set() - for potential in potentials: - if not potential: - continue - parsed = from_text(potential) - if parsed: - versions.add(parsed) - - if len(versions) > 1: - raise ValueError("conflicting versions within project: %s" % versions) - - if not versions: - _LOG.debug("key pairs found: \n%r", known) - raise ValueError("could not find existing semver") - return versions.pop() - - -def make_new_semver(current_semver, all_triggers, **overrides): - """Defines how to increment semver based on which significant figure is triggered""" - new_semver = {} - bumped = False - for sig_fig in SemVerSigFig: # iterate sig figs in order of significance - value = getattr(current_semver, sig_fig) - override = overrides.get(sig_fig) - if override is not None: - new_semver[sig_fig] = override - if int(override) > int(value): - bumped = True - elif bumped: - new_semver[sig_fig] = "0" - elif sig_fig in all_triggers: - new_semver[sig_fig] = str(int(value) + 1) - bumped = True - else: - new_semver[sig_fig] = value - return SemVer(**new_semver) diff --git a/src/auto_version/tests/example.py b/src/auto_version/tests/example.py index 3372292..a2ca29e 100644 --- a/src/auto_version/tests/example.py +++ b/src/auto_version/tests/example.py @@ -1,4 +1,5 @@ LOCK = False RELEASE = True -VERSION = "19.99.0.devX" -VERSION_AGAIN = "19.99.0.devX" +VERSION = "19.99.0" +VERSION_AGAIN = "19.99.0" +STRICT_VERSION = "19.99.0" diff --git a/src/auto_version/tests/example.toml b/src/auto_version/tests/example.toml index 7795eef..642e704 100644 --- a/src/auto_version/tests/example.toml +++ b/src/auto_version/tests/example.toml @@ -1,11 +1,12 @@ [AutoVersionConfig] CONFIG_NAME = 'example' -DEVMODE_TEMPLATE = "{version}.devX" +PRERELEASE_TOKEN = 'dev' targets = ["example.py"] [AutoVersionConfig.key_aliases] VERSION = "VERSION_KEY" VERSION_AGAIN = "VERSION_KEY" +STRICT_VERSION = "VERSION_KEY_STRICT" LOCK = "VERSION_LOCK" RELEASE = "RELEASE_FIELD" diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index ce96fa7..52e4147 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -1,21 +1,20 @@ -import imp import contextlib -import subprocess -import os -import unittest import functools -import shlex +import imp +import os import re +import shlex +import subprocess +import unittest import six - -from auto_version.auto_version_tool import main +from auto_version import auto_version_tool from auto_version.auto_version_tool import extract_keypairs +from auto_version.auto_version_tool import main from auto_version.auto_version_tool import replace_lines -from auto_version import auto_version_tool -from auto_version.replacement_handler import ReplacementHandler from auto_version.config import AutoVersionConfig as config from auto_version.config import Constants +from auto_version.replacement_handler import ReplacementHandler class TestBumps(unittest.TestCase): @@ -32,29 +31,77 @@ def tearDown(self): 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"} + updates, + { + "RELEASE": True, + "VERSION": "19.99.1", + "VERSION_AGAIN": "19.99.1", + "STRICT_VERSION": "19.99.1", + }, ) def test_bump_major(self): old, new, updates = self.call(bump="major", release=True) self.assertEqual( - updates, {"RELEASE": True, "VERSION": "20.0.0", "VERSION_AGAIN": "20.0.0"} + updates, + { + "RELEASE": True, + "VERSION": "20.0.0", + "VERSION_AGAIN": "20.0.0", + "STRICT_VERSION": "20.0.0", + }, ) def test_bump_news(self): old, new, updates = self.call(file_triggers=True, release=True) self.assertEqual( updates, - {"RELEASE": True, "VERSION": "19.100.0", "VERSION_AGAIN": "19.100.0"}, + { + "RELEASE": True, + "VERSION": "19.100.0", + "VERSION_AGAIN": "19.100.0", + "STRICT_VERSION": "19.100.0", + }, ) def test_dev(self): - old, new, updates = self.call() + old, new, updates = self.call(bump="prerelease") + self.assertEqual( + updates, + { + "VERSION": "19.99.0-dev.1", + "VERSION_AGAIN": "19.99.0-dev.1", + "STRICT_VERSION": "19.99.0", + }, + ) + + def test_build(self): + old, new, updates = self.call(bump="build") self.assertEqual( - updates, {"VERSION": "19.99.0.devX", "VERSION_AGAIN": "19.99.0.devX"} + updates, + { + "VERSION": "19.99.0+build.1", + "VERSION_AGAIN": "19.99.0+build.1", + "STRICT_VERSION": "19.99.0", + }, + ) + + def test_non_release_bump(self): + old, new, updates = self.call(bump="minor") + self.assertEqual( + updates, + { + "VERSION": "19.100.0-dev.1", + "VERSION_AGAIN": "19.100.0-dev.1", + "STRICT_VERSION": "19.100.0", + }, ) - def test_increment_prerelease(self): + def test_invalid_bump(self): + with self.assertRaises(KeyError): + self.call(bump="banana") + + def test_increment_existing_prerelease(self): old, new, updates = self.call(set_to="1.2.3-RC.1") self.assertEqual(new, "1.2.3-RC.1") old, new, updates = self.call(bump="prerelease") @@ -64,7 +111,7 @@ def test_end_to_end(self): self.call(bump="major") filepath = os.path.join(os.path.dirname(__file__), "example.py") example = imp.load_source("example", filepath) - self.assertEqual(example.VERSION, "20.0.0.devX") + self.assertEqual(example.VERSION, "20.0.0-dev.1") class TestVCSTags(unittest.TestCase): @@ -87,40 +134,54 @@ def tearDown(self): cmd = "git tag --delete release/4.5.6" subprocess.check_call(shlex.split(cmd)) try: - cmd = "git tag --delete release/5.0.0.devX" + 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.devX" - old, new, updates = self.call(persist_from=Constants.FROM_VCS_ANCESTOR, bump='major') + bumped = "5.0.0-dev.1" + old, new, updates = self.call( + persist_from=Constants.FROM_VCS_ANCESTOR, bump="major" + ) self.assertEqual( - updates, {"VERSION": bumped, "VERSION_AGAIN": bumped} + updates, + {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, ) 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.devX" - old, new, updates = self.call(persist_from=Constants.FROM_VCS_LATEST, bump='major') + bumped = "5.0.0-dev.1" + old, new, updates = self.call( + persist_from=Constants.FROM_VCS_LATEST, bump="major" + ) self.assertEqual( - updates, {"VERSION": bumped, "VERSION_AGAIN": bumped} + updates, + {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, ) def test_to_tag(self): """writes a tag in git (TODO: but we cant test global tags without making a new branch etc etc) """ - bumped = "5.0.0.devX" - old, new, updates = self.call(persist_from=Constants.FROM_VCS_LATEST, persist_to=[Constants.TO_VCS], bump='major') + bumped = "5.0.0-dev.1" + old, new, updates = self.call( + persist_from=Constants.FROM_VCS_LATEST, + persist_to=[Constants.TO_VCS], + bump="major", + ) self.assertEqual( - updates, {"VERSION": bumped, "VERSION_AGAIN": bumped} + updates, + {"VERSION": bumped, "VERSION_AGAIN": bumped, "STRICT_VERSION": "5.0.0"}, ) version = auto_version_tool.get_dvcs_latest_tag_semver() - self.assertEqual(dict(version._asdict()), dict(major='5', minor='0', patch='0')) + self.assertEqual( + dict(version._asdict()), + dict(major=5, minor=0, patch=0, build=None, prerelease="dev.1"), + ) @contextlib.contextmanager diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py new file mode 100644 index 0000000..77304df --- /dev/null +++ b/src/auto_version/utils.py @@ -0,0 +1,103 @@ +"""Functions for manipulating SemVer objects (Major.Minor.Patch)""" +import logging + +import semver +from auto_version.config import AutoVersionConfig as config +from auto_version.config import Constants +from auto_version.definitions import SemVerSigFig + +_LOG = logging.getLogger(__file__) + + +def from_text_or_none(text): + """A version or None""" + if text is not None: + try: + return semver.parse_version_info(text) + except ValueError: + _LOG.debug("version string is not semver-compatible: %r", text) + pass + + +def get_semver_from_source(data): + """Given a dictionary of all version data available, determine the current version""" + # get the not-none values from data + known = { + key: data.get(alias) + for key, alias in config._forward_aliases.items() + if data.get(alias) is not None + } + + # prefer the non-strict field, if available, because it retains more information + potentials = [ + known.pop(Constants.VERSION_FIELD, None), + known.pop(Constants.VERSION_STRICT_FIELD, None), + ] + + # build from components, if they're defined + from_components = {k: known.get(k) for k in SemVerSigFig if k in known} + try: + potentials.append(str(semver.VersionInfo(**from_components))) + except TypeError: + # we didn't have enough components + pass + + versions = [potential for potential in potentials if from_text_or_none(potential)] + release_versions = {semver.finalize_version(potential) for potential in potentials} + + if len(release_versions) > 1: + raise ValueError("conflicting versions within project: %s" % versions) + + if not versions: + _LOG.debug("key pairs found: \n%r", known) + raise ValueError("could not find existing semver") + return versions[0] + + +def get_token_args(sig_fig): + token_args = {} + if sig_fig == SemVerSigFig.build: + token_args = {"token": config.BUILD_TOKEN} + if sig_fig == SemVerSigFig.prerelease: + token_args = {"token": config.PRERELEASE_TOKEN} + return token_args + + +def make_new_semver(version_string, 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 overrides: explicit values for some or all of the sigfigs + :return: + """ + + # 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 + ) + + # perform any explicit setting of parts + 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 From fa9d69e11e2f0489adfe0cd10d5f1ec12863c564 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 27 Feb 2019 14:19:24 +0000 Subject: [PATCH 05/28] move semver requirement to requirements.txt --- Pipfile | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 72d2fb8..6f93db8 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,6 @@ name = "pypi" [packages] # defer to setup.py pyautoversion = {path = ".",editable = true} -semver = "*" [dev-packages] "flake8" = "*" diff --git a/requirements.txt b/requirements.txt index bd79a65..b7c9825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ toml +semver From a174d86d196d9103c00d7b2023a4c9ff24a91ebb Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 28 Feb 2019 10:04:10 +0000 Subject: [PATCH 06/28] 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 --- src/auto_version/auto_version_tool.py | 5 +++++ src/auto_version/tests/example.py | 1 + src/auto_version/tests/simple.toml | 6 ++++++ src/auto_version/tests/test_autoversion.py | 10 ++++++++++ src/auto_version/utils.py | 12 ++++++++---- 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/auto_version/tests/simple.toml diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index f7e2bba..ce76823 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -92,6 +92,7 @@ def read_targets(targets): for target, regexer in regexer_for_targets(targets): with open(target) as fh: results.update(extract_keypairs(fh.readlines(), regexer)) + _LOG.debug("found the following key-value pairs in source: %r", results) return results @@ -287,6 +288,10 @@ def main( # 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 diff --git a/src/auto_version/tests/example.py b/src/auto_version/tests/example.py index a2ca29e..f5e9279 100644 --- a/src/auto_version/tests/example.py +++ b/src/auto_version/tests/example.py @@ -3,3 +3,4 @@ VERSION = "19.99.0" VERSION_AGAIN = "19.99.0" STRICT_VERSION = "19.99.0" +UNRELATED_STRING = "apple" diff --git a/src/auto_version/tests/simple.toml b/src/auto_version/tests/simple.toml new file mode 100644 index 0000000..d202870 --- /dev/null +++ b/src/auto_version/tests/simple.toml @@ -0,0 +1,6 @@ +[AutoVersionConfig] +PRERELEASE_TOKEN = 'dev' +targets = ["example.py"] + +[AutoVersionConfig.key_aliases] +VERSION_AGAIN = "VERSION_KEY" diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 52e4147..23b6573 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -113,6 +113,16 @@ def test_end_to_end(self): example = imp.load_source("example", filepath) self.assertEqual(example.VERSION, "20.0.0-dev.1") + def test_simple_config_bump(self): + old, new, updates = self.call(config_path="simple.toml", bump="minor") + self.assertEqual(new, "19.100.0-dev.1") + # do our own teardown... + self.call(config_path="simple.toml", set_to="19.99.0") + + def test_custom_field_set(self): + old, new, updates = self.call(UNRELATED_STRING="apple") + self.assertEqual(updates["UNRELATED_STRING"], "apple") + class TestVCSTags(unittest.TestCase): call = functools.partial(main, config_path="example.toml") diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 77304df..8c5f2dd 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -27,11 +27,12 @@ def get_semver_from_source(data): for key, alias in config._forward_aliases.items() if data.get(alias) is not None } + _LOG.debug("valid, mapped keys: %r", known) # prefer the non-strict field, if available, because it retains more information potentials = [ - known.pop(Constants.VERSION_FIELD, None), - known.pop(Constants.VERSION_STRICT_FIELD, None), + known.get(Constants.VERSION_FIELD, None), + known.get(Constants.VERSION_STRICT_FIELD, None), ] # build from components, if they're defined @@ -43,10 +44,13 @@ def get_semver_from_source(data): pass versions = [potential for potential in potentials if from_text_or_none(potential)] - release_versions = {semver.finalize_version(potential) for potential in potentials} + release_versions = {semver.finalize_version(version) for version in versions} if len(release_versions) > 1: - raise ValueError("conflicting versions within project: %s" % versions) + raise ValueError( + "conflicting versions within project: %s\nkeys were: %r" + % (release_versions, known) + ) if not versions: _LOG.debug("key pairs found: \n%r", known) From 6c9506dd01ba2b79f5c67b12110253de95146ec9 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 28 Feb 2019 11:55:18 +0000 Subject: [PATCH 07/28] support for detecting newsfiles added since last release --- docs/news/infinite.feature | 2 + src/auto_version/auto_version_tool.py | 122 ++++++++++++++++----- src/auto_version/cli.py | 6 + src/auto_version/tests/test_autoversion.py | 2 +- src/auto_version/utils.py | 7 +- 5 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 docs/news/infinite.feature diff --git a/docs/news/infinite.feature b/docs/news/infinite.feature new file mode 100644 index 0000000..b7ae49b --- /dev/null +++ b/docs/news/infinite.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/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index ce76823..aec3da9 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -96,26 +96,63 @@ def read_targets(targets): return results -def detect_file_triggers(trigger_patterns): +def detect_file_triggers(release_commit): """The existence of files matching configured globs will trigger a version bump""" triggers = set() - for trigger, pattern in trigger_patterns.items(): + for trigger, pattern in config.trigger_patterns.items(): matches = glob.glob(pattern) + if matches: - _LOG.debug("trigger: %s bump from %r\n\t%s", trigger, pattern, matches) - triggers.add(trigger) + added_files = set(matches) + if release_commit: + # if we have a specific release commit, we will additionally filter + # to ensure that only files that were added since that commit are considered + # this allows the project to retain newsfiles for all time, rather than having to delete them + + # fortunately, git filter syntax is compatible with the glob syntax we're already using + git_response = ( + subprocess.check_output( + [ + "git", + "diff", + "--name-status", + release_commit, + "HEAD", + "--diff-filter", + "A", + pattern, + ] + ) + .decode("utf8") + .strip() + .splitlines() + ) + file_paths = [path.split()[1].strip() for path in git_response] + _LOG.debug("added since last release: %r", file_paths) + added_files = set(file_paths) + + # perform the additional filtering + valid_news = added_files.intersection(matches) + if valid_news: + _LOG.debug( + "trigger: %s bump from %r\n\t%s", trigger, pattern, valid_news + ) + triggers.add(trigger) + else: + _LOG.debug("trigger: no match on %r because files aren't new", pattern) else: _LOG.debug("trigger: no match on %r", pattern) return triggers -def get_all_triggers(bump, file_triggers): +def get_all_triggers(bump, enable_file_triggers, release_commit): """Aggregated set of significant figures to bump""" triggers = set() - if file_triggers: - triggers = triggers.union(detect_file_triggers(config.trigger_patterns)) + if enable_file_triggers: + triggers = triggers.union(detect_file_triggers(release_commit)) if bump: _LOG.debug("trigger: %s bump requested", bump) + _ = definitions.SemVerSigFig._asdict()[bump] triggers.add(bump) return triggers @@ -184,9 +221,34 @@ def get_all_versions_from_tags(tags): return matches +def get_sha_from_version(version, persist_from): + """Given a previously tagged release version (and the tag template) + + Find the commit of that version + """ + if persist_from == Constants.FROM_SOURCE: + return None + try: + result = ( + subprocess.check_output( + [ + "git", + "rev-parse", + "--verify", + config.TAG_TEMPLATE.format(version=version), + ] + ) + .decode("utf8") + .strip() + ) + _LOG.debug("the commit of the last release is %s", result) + return result + except subprocess.CalledProcessError: + _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""" - # TODO: limitation of our library in general: we don't understand prerelease versions 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()) @@ -196,15 +258,20 @@ def get_dvcs_latest_tag_semver(): ordered_versions = sorted( set(utils.from_text_or_none(version) for version in matches) ) - _LOG.debug("matched tag versions %s", ordered_versions) - return ordered_versions.pop() + result = None + if ordered_versions: + result = ordered_versions.pop() + _LOG.info("latest version found in across all dvcs tags: %s", result) + return result 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()) - return utils.from_text_or_none(get_all_versions_from_tags([version])[0]) + 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 add_dvcs_tag(version): @@ -243,10 +310,11 @@ def main( release=None, bump=None, lock=None, - file_triggers=None, + enable_file_triggers=None, config_path=None, persist_from=Constants.FROM_SOURCE, persist_to=None, + dry_run=None, **extra_updates ): """Main workflow. @@ -265,22 +333,15 @@ def main( more significant bumps will zero the less significant ones :param lock: locks the version string for the next call to autoversion lock only removed if a version bump would have occurred - :param file_triggers: whether to enable bumping based on file triggers + :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 config_path: path to config file :param extra_updates: :return: """ updates = {} - persist_to = persist_to or [Constants.TO_SOURCE] - - if config_path: - get_or_create_config(config_path, config) - - # perform some input validation - if bump: - _ = definitions.SemVerSigFig._asdict()[bump] + get_or_create_config(config_path, config) for k, v in config.regexers.items(): config.regexers[k] = re.compile(v) @@ -297,8 +358,9 @@ def main( all_data = {} current_semver = get_current_version(persist_from) + release_commit = get_sha_from_version(current_semver, persist_from) new_semver = current_semver = str(current_semver) - triggers = get_all_triggers(bump, file_triggers) + triggers = get_all_triggers(bump, enable_file_triggers, release_commit) updates.update(get_lock_behaviour(triggers, all_data, lock)) updates.update(get_dvcs_info()) @@ -333,11 +395,14 @@ def main( # finally, add in commandline overrides native_updates.update(extra_updates) - if Constants.TO_SOURCE in persist_to: - write_targets(config.targets, **native_updates) + if not dry_run: + if Constants.TO_SOURCE in persist_to: + write_targets(config.targets, **native_updates) - if Constants.TO_VCS in persist_to: - add_dvcs_tag(updates[Constants.VERSION_FIELD]) + 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 @@ -383,8 +448,11 @@ def main_from_cli(): lock=args.lock, release=args.release, bump=args.bump, - file_triggers=args.file_triggers, + enable_file_triggers=args.file_triggers, config_path=args.config, + dry_run=args.show, + persist_from=args.persist_from, + persist_to=args.persist_to, **command_line_updates ) _LOG.info("previously: %s", old) diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 8e27fe2..8728b00 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -13,6 +13,12 @@ def get_cli(): prog="auto_version", description="auto version v%s: a tool to control version numbers" % __version__, ) + parser.add_argument( + "--show", + "--dry-run", + action="store_true", + help="Don't write anything to disk or vcs.", + ) parser.add_argument( "--bump", choices=SemVerSigFig, diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 23b6573..a0c0fc4 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -53,7 +53,7 @@ def test_bump_major(self): ) def test_bump_news(self): - old, new, updates = self.call(file_triggers=True, release=True) + old, new, updates = self.call(enable_file_triggers=True, release=True) self.assertEqual( updates, { diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 8c5f2dd..a247a35 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -55,7 +55,12 @@ def get_semver_from_source(data): if not versions: _LOG.debug("key pairs found: \n%r", known) raise ValueError("could not find existing semver") - return versions[0] + + result = None + if versions: + result = versions[0] + _LOG.info("latest version found in source: %r", result) + return result def get_token_args(sig_fig): From dfa6c7d010eb302bc36f6c913aa16258041b212f Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 28 Feb 2019 13:02:11 +0000 Subject: [PATCH 08/28] add ability to extract the names of file triggers --- src/auto_version/auto_version_tool.py | 28 +++++++++++++++++++-------- src/auto_version/cli.py | 5 +++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index aec3da9..af0b127 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -98,6 +98,7 @@ def read_targets(targets): 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(): matches = glob.glob(pattern) @@ -138,18 +139,20 @@ def detect_file_triggers(release_commit): "trigger: %s bump from %r\n\t%s", trigger, pattern, valid_news ) triggers.add(trigger) + all_valid_trigger_files.update(valid_news) else: _LOG.debug("trigger: no match on %r because files aren't new", pattern) else: _LOG.debug("trigger: no match on %r", pattern) - return triggers + return triggers, all_valid_trigger_files def get_all_triggers(bump, enable_file_triggers, release_commit): """Aggregated set of significant figures to bump""" triggers = set() if enable_file_triggers: - triggers = triggers.union(detect_file_triggers(release_commit)) + file_triggers, _ = detect_file_triggers(release_commit) + triggers.update(file_triggers) if bump: _LOG.debug("trigger: %s bump requested", bump) _ = definitions.SemVerSigFig._asdict()[bump] @@ -221,7 +224,7 @@ def get_all_versions_from_tags(tags): return matches -def get_sha_from_version(version, persist_from): +def get_dvcs_commit_for_version(version, persist_from): """Given a previously tagged release version (and the tag template) Find the commit of that version @@ -358,7 +361,7 @@ def main( all_data = {} current_semver = get_current_version(persist_from) - release_commit = get_sha_from_version(current_semver, 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)) @@ -458,10 +461,19 @@ def main_from_cli(): _LOG.info("previously: %s", old) _LOG.info("currently: %s", new) _LOG.debug("updates:\n%s", pprint.pformat(updates)) - print( - updates.get(config._forward_aliases.get(Constants.VERSION_FIELD)) - or updates.get(config._forward_aliases.get(Constants.VERSION_STRICT_FIELD)) - ) + + version = updates.get( + config._forward_aliases.get(Constants.VERSION_FIELD) + ) or updates.get(config._forward_aliases.get(Constants.VERSION_STRICT_FIELD)) + + if args.print_file_triggers: + commit = get_dvcs_commit_for_version( + persist_from=args.persist_from, version=version + ) + _, files = detect_file_triggers(commit) + print("\n".join(files)) + else: + print(version) __name__ == "__main__" and main_from_cli() diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 8728b00..db2f250 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( + "--print-file-triggers", + action="store_true", + help="Prints a newline separated list of files detected as bump triggers.", + ) parser.add_argument( "--set", help="Set the SemVer string. Use this locally to set the project version explicitly.", From c01877e3bdbe020d15631862e07deaa5d70e717c Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 28 Feb 2019 14:25:37 +0000 Subject: [PATCH 09/28] allow multiple sources gives better handling of situation where user is starting from `0.0.0` with no tags. --- src/auto_version/auto_version_tool.py | 35 ++++++++++++---------- src/auto_version/cli.py | 9 +++--- src/auto_version/tests/test_autoversion.py | 6 ++-- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index af0b127..72e36dc 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -229,7 +229,7 @@ def get_dvcs_commit_for_version(version, persist_from): Find the commit of that version """ - if persist_from == Constants.FROM_SOURCE: + if persist_from == [Constants.FROM_SOURCE]: return None try: result = ( @@ -259,7 +259,7 @@ def get_dvcs_latest_tag_semver(): _LOG.debug("all tags matching simple pattern %r : %s", tag_glob, tags) matches = get_all_versions_from_tags(tags) ordered_versions = sorted( - set(utils.from_text_or_none(version) for version in matches) + {v for v in set(utils.from_text_or_none(version) for version in matches) if v} ) result = None if ordered_versions: @@ -288,13 +288,19 @@ def add_dvcs_tag(version): def get_current_version(persist_from): - if persist_from == Constants.FROM_SOURCE: - all_data = read_targets(config.targets) - return utils.get_semver_from_source(all_data) - elif persist_from == Constants.FROM_VCS_LATEST: - return get_dvcs_latest_tag_semver() - elif persist_from == Constants.FROM_VCS_ANCESTOR: - return get_dvcs_ancestor_tag_semver() + """Try loading the version from the sources in the order provided to us""" + version = None + for source in 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() + if version: + break + return version def get_overrides(updates, commit_count_as): @@ -315,7 +321,7 @@ def main( lock=None, enable_file_triggers=None, config_path=None, - persist_from=Constants.FROM_SOURCE, + persist_from=None, persist_to=None, dry_run=None, **extra_updates @@ -344,6 +350,7 @@ 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(): @@ -462,18 +469,14 @@ def main_from_cli(): _LOG.info("currently: %s", new) _LOG.debug("updates:\n%s", pprint.pformat(updates)) - version = updates.get( - config._forward_aliases.get(Constants.VERSION_FIELD) - ) or updates.get(config._forward_aliases.get(Constants.VERSION_STRICT_FIELD)) - if args.print_file_triggers: commit = get_dvcs_commit_for_version( - persist_from=args.persist_from, version=version + persist_from=args.persist_from, version=old ) _, files = detect_file_triggers(commit) print("\n".join(files)) else: - print(version) + print(new) __name__ == "__main__" and main_from_cli() diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index db2f250..158cc7f 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -71,15 +71,16 @@ def get_cli(): Constants.FROM_VCS_ANCESTOR, Constants.FROM_VCS_LATEST, }, - default=Constants.FROM_SOURCE, - help="Where the current version is stored. This is the version that will be incremented.", + action="append", + default=[], + help="Where the current version is stored. Looks for each source in order. (default: source files)", ) parser.add_argument( "--persist-to", action="append", choices={Constants.TO_SOURCE, Constants.TO_VCS}, - default=[Constants.TO_SOURCE], - help="Where the new version is stored. This could be in multiple places at once.", + default=[], + help="Where the new version is stored. This could be in multiple places at once. (default: source files)", ) default_config_file_path = os.path.join(os.getcwd(), "pyproject.toml") parser.add_argument( diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index a0c0fc4..9a2b9e8 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -153,7 +153,7 @@ def test_from_ancestor_tag(self): """i.e. most immediate ancestor tag""" bumped = "5.0.0-dev.1" old, new, updates = self.call( - persist_from=Constants.FROM_VCS_ANCESTOR, bump="major" + persist_from=[Constants.FROM_VCS_ANCESTOR], bump="major" ) self.assertEqual( updates, @@ -166,7 +166,7 @@ def test_from_latest_of_all_time(self): """ bumped = "5.0.0-dev.1" old, new, updates = self.call( - persist_from=Constants.FROM_VCS_LATEST, bump="major" + persist_from=[Constants.FROM_VCS_LATEST], bump="major" ) self.assertEqual( updates, @@ -179,7 +179,7 @@ def test_to_tag(self): """ bumped = "5.0.0-dev.1" old, new, updates = self.call( - persist_from=Constants.FROM_VCS_LATEST, + persist_from=[Constants.FROM_VCS_LATEST], persist_to=[Constants.TO_VCS], bump="major", ) From 8d36de2fce97e52776ca687ea56831d73507403f Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 5 Mar 2019 15:58:52 +0000 Subject: [PATCH 10/28] fix bug where git was returning files relative to project root whereas glob returns files relative to cwd --- src/auto_version/auto_version_tool.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 72e36dc..168e1db 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -104,7 +104,7 @@ def detect_file_triggers(release_commit): matches = glob.glob(pattern) if matches: - added_files = set(matches) + valid_news = set(matches) if release_commit: # if we have a specific release commit, we will additionally filter # to ensure that only files that were added since that commit are considered @@ -116,6 +116,7 @@ def detect_file_triggers(release_commit): [ "git", "diff", + "--relative", "--name-status", release_commit, "HEAD", @@ -129,11 +130,10 @@ def detect_file_triggers(release_commit): .splitlines() ) file_paths = [path.split()[1].strip() for path in git_response] - _LOG.debug("added since last release: %r", file_paths) - added_files = set(file_paths) + _LOG.debug("trigger: added since last release: %r", file_paths) + valid_news.intersection_update(set(file_paths)) # perform the additional filtering - valid_news = added_files.intersection(matches) if valid_news: _LOG.debug( "trigger: %s bump from %r\n\t%s", trigger, pattern, valid_news @@ -141,7 +141,11 @@ def detect_file_triggers(release_commit): triggers.add(trigger) all_valid_trigger_files.update(valid_news) else: - _LOG.debug("trigger: no match on %r because files aren't new", pattern) + _LOG.debug( + "trigger: no match on %r because files aren't new: %s", + pattern, + matches, + ) else: _LOG.debug("trigger: no match on %r", pattern) return triggers, all_valid_trigger_files From 2fafb2fc2eabe60f9a1406deece1b79cb3faa23b Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 2 Apr 2019 18:07:22 +0100 Subject: [PATCH 11/28] 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", ``` --- docs/news/inv-conf.major | 2 ++ src/auto_version/auto_version_tool.py | 2 +- src/auto_version/config.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 docs/news/inv-conf.major diff --git a/docs/news/inv-conf.major b/docs/news/inv-conf.major new file mode 100644 index 0000000..8482ce8 --- /dev/null +++ b/docs/news/inv-conf.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/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 168e1db..ba3cf7f 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -100,7 +100,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: diff --git a/src/auto_version/config.py b/src/auto_version/config.py index a6df16d..a446e5c 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -62,9 +62,9 @@ class AutoVersionConfig(object): ".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" From 1de6a56b92a0678a555f351b394cbf33189601b8 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 3 Apr 2019 17:34:51 +0100 Subject: [PATCH 12/28] provide a persist-from for previous release --- src/auto_version/auto_version_tool.py | 31 ++++++++++++++++++++++++--- src/auto_version/cli.py | 1 + src/auto_version/config.py | 1 + src/auto_version/utils.py | 5 ++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index ba3cf7f..f251708 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -254,8 +254,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()) @@ -265,13 +269,32 @@ 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} ) + return ordered_versions + + +def get_dvcs_latest_tag_semver(): + ordered_versions = get_dvcs_ordered_tag_semvers() result = None if ordered_versions: result = ordered_versions.pop() - _LOG.info("latest version found in across all dvcs tags: %s", result) + _LOG.info("latest version found across all dvcs tags: %s", result) return result +def get_dvcs_previous_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 version.build or version.prerelease: + continue + else: + break + else: + version = None + _LOG.info("previous release found across all dvcs tags: %s", version) + return version + + def get_dvcs_ancestor_tag_semver(): """Gets the latest tag that's an ancestor to the current commit""" cmd = "git describe --abbrev=0 --tags" @@ -302,6 +325,8 @@ def get_current_version(persist_from): version = get_dvcs_latest_tag_semver() elif source == Constants.FROM_VCS_ANCESTOR: version = get_dvcs_ancestor_tag_semver() + elif source == Constants.FROM_VCS_PREVIOUS_RELEASE: + version = get_dvcs_previous_release_semver() if version: break return version diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 158cc7f..cb19a7e 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -70,6 +70,7 @@ def get_cli(): Constants.FROM_SOURCE, Constants.FROM_VCS_ANCESTOR, Constants.FROM_VCS_LATEST, + Constants.FROM_VCS_PREVIOUS_RELEASE, }, action="append", default=[], diff --git a/src/auto_version/config.py b/src/auto_version/config.py index a446e5c..2715024 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -27,6 +27,7 @@ class Constants(object): FROM_SOURCE = "source" FROM_VCS_ANCESTOR = "vcs" FROM_VCS_LATEST = "vcs-latest" + FROM_VCS_PREVIOUS_RELEASE = "vcs-previous-release" TO_SOURCE = "source" TO_VCS = "vcs" diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index a247a35..3e6779a 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) From 49f1f97909aaece7a8427f413152986d0a6b094d Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 3 Apr 2019 18:37:24 +0100 Subject: [PATCH 13/28] add functions for comparing semvers --- src/auto_version/utils.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 3e6779a..4b157a3 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -75,6 +75,20 @@ def get_token_args(sig_fig): return token_args +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 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 make_new_semver(version_string, all_triggers, **overrides): """Defines how to increment semver based on which significant figure is triggered (most significant takes precendence) @@ -87,14 +101,12 @@ def make_new_semver(version_string, all_triggers, **overrides): # 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 + bump_sigfig = max_sigfig(all_triggers) + if bump_sigfig in (SemVerSigFig.prerelease, SemVerSigFig.build): + also_prerelease = False + version_string = getattr(semver, "bump_" + bump_sigfig)( + version_string, **get_token_args(bump_sigfig) + ) if also_prerelease: # if we *didnt* increment sub-patch, then we should do so From 024b435a25eb9e84a1522892074b45f595263dc5 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 4 Apr 2019 13:19:30 +0100 Subject: [PATCH 14/28] oops, fix test for trigger config --- src/auto_version/tests/example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From f84d77fee6905c745601fee30569e7bc5c3028f8 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 4 Apr 2019 13:20:00 +0100 Subject: [PATCH 15/28] add previous-release comparison logic --- src/auto_version/auto_version_tool.py | 40 ++++++++++++++++----------- src/auto_version/cli.py | 5 ++++ src/auto_version/config.py | 1 + src/auto_version/utils.py | 39 ++++++++++++++++++++------ 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index f251708..6f8b2c8 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -181,7 +181,7 @@ def get_lock_behaviour(triggers, all_data, lock): return updates -def get_final_version_string(release_mode, version): +def get_finalised_updates(release_mode, version): """Generates update dictionary entries for the version string""" production_version = semver.finalize_version(version) updates = {} @@ -285,9 +285,7 @@ def get_dvcs_previous_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 version.build or version.prerelease: - continue - else: + if utils.is_release(version): break else: version = None @@ -349,6 +347,7 @@ def main( bump=None, lock=None, enable_file_triggers=None, + incr_from_release=None, config_path=None, persist_from=None, persist_to=None, @@ -364,7 +363,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 @@ -373,6 +372,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: @@ -396,54 +403,54 @@ def main( config._forward_aliases[v] = k all_data = {} + last_release_semver = incr_from_release and get_dvcs_previous_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)) + updates.update(get_finalised_updates(release_mode=release, version=str(new_version))) # 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): @@ -488,6 +495,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 cb19a7e..1d21309 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( + "--news-since-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", diff --git a/src/auto_version/config.py b/src/auto_version/config.py index 2715024..12f1452 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -70,6 +70,7 @@ class AutoVersionConfig(object): 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/utils.py b/src/auto_version/utils.py index 4b157a3..0fcaf13 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -63,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): @@ -82,6 +82,13 @@ def max_sigfig(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: @@ -89,23 +96,40 @@ def semver_diff(semver1, semver2): return sig_fig -def make_new_semver(version_string, all_triggers, **overrides): +def sigfig_gt(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: """ + # if the current version isn't a full release, 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 not is_release(current_semver) and last_release_semver: + if sigfig_gt(max_sigfig(all_triggers), semver_diff(current_semver, last_release_semver)): + min_incr = min_sigfig(all_triggers) + all_triggers.clear().append(min_incr) + # perform an increment using the most-significant trigger also_prerelease = True bump_sigfig = max_sigfig(all_triggers) if bump_sigfig in (SemVerSigFig.prerelease, SemVerSigFig.build): also_prerelease = False version_string = getattr(semver, "bump_" + bump_sigfig)( - version_string, **get_token_args(bump_sigfig) + str(current_semver), **get_token_args(bump_sigfig) ) if also_prerelease: @@ -122,6 +146,5 @@ def make_new_semver(version_string, all_triggers, **overrides): 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 be5c24e71554593ac866b4f21efaf94f2bfa24bb Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 4 Apr 2019 15:41:57 +0100 Subject: [PATCH 16/28] tests and code for correctly incrementing, given a previous release --- docs/news/incr-flow.feature | 1 + src/auto_version/auto_version_tool.py | 33 ++++++----- src/auto_version/tests/test_autoversion.py | 64 ++++++++++++++++++++++ src/auto_version/utils.py | 47 +++++++++------- 4 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 docs/news/incr-flow.feature diff --git a/docs/news/incr-flow.feature b/docs/news/incr-flow.feature new file mode 100644 index 0000000..49d362a --- /dev/null +++ b/docs/news/incr-flow.feature @@ -0,0 +1 @@ +Adds better workflow for incrementing patches. \ No newline at end of file diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 6f8b2c8..a016229 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -340,6 +340,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, @@ -387,20 +405,7 @@ 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 = incr_from_release and get_dvcs_previous_release_semver() diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 9a2b9e8..d2a4889 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -1,4 +1,5 @@ import contextlib +import semver import functools import imp import os @@ -9,6 +10,7 @@ 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 main from auto_version.auto_version_tool import replace_lines @@ -124,6 +126,68 @@ def test_custom_field_set(self): self.assertEqual(updates["UNRELATED_STRING"], "apple") +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_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") diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 0fcaf13..968ad8d 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -101,6 +101,11 @@ def sigfig_gt(sig_fig1, sig_fig2): 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) @@ -115,32 +120,36 @@ def make_new_semver(current_semver, last_release_semver, all_triggers, **overrid :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, 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 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)): - min_incr = min_sigfig(all_triggers) - all_triggers.clear().append(min_incr) + # 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 bump_sigfig = max_sigfig(all_triggers) - if bump_sigfig in (SemVerSigFig.prerelease, SemVerSigFig.build): - also_prerelease = False - version_string = getattr(semver, "bump_" + bump_sigfig)( - str(current_semver), **get_token_args(bump_sigfig) - ) - - 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 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) From ab8aec8b4dd1ea026829def2ea24f0a72c2668f5 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 4 Apr 2019 16:38:31 +0100 Subject: [PATCH 17/28] use the right param name in the cli --- src/auto_version/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index 1d21309..d66fbd9 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -33,7 +33,7 @@ def get_cli(): help="Detects need to bump based on presence of files (as specified in config).", ) parser.add_argument( - "--news-since-release", + "--incr-from-release", action="store_true", help="Automatically sets version number based on SCIENCE (see docs). Requires use of VCS tags.", ) From d418cc8801ec4cf64be319f562aa9ff26755f2a1 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Mon, 8 Apr 2019 12:27:03 +0100 Subject: [PATCH 18/28] log which previous release was detected --- src/auto_version/auto_version_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index a016229..333deb7 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -409,6 +409,7 @@ def main( all_data = {} last_release_semver = incr_from_release and get_dvcs_previous_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) triggers = get_all_triggers(bump, enable_file_triggers, release_commit) From f83e1d1667d233aff44a6ab98ab46f94ce397407 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 10 Apr 2019 17:38:37 +0100 Subject: [PATCH 19/28] minimum patch increment for an existing version --- src/auto_version/tests/test_autoversion.py | 31 +++++++++++++++------- src/auto_version/utils.py | 7 ++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index d2a4889..5f614cb 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -71,19 +71,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", }, ) @@ -170,22 +172,31 @@ def check(self, previous, current, bumps, expect): ) def test_release_bump(self): - self.check(None, "1.2.3", ["minor"], "1.3.0-dev.1") + 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") + 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") + 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") + 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") + self.check("1.2.2", "1.2.3-dev.1", {"minor"}, "1.3.0-dev.1") class TestVCSTags(unittest.TestCase): diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 968ad8d..3a39d32 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -131,7 +131,12 @@ def make_new_semver(current_semver, last_release_semver, all_triggers, **overrid pass else: # here the changes are same or lesser than the original RC bump, so we only bump prerelease - all_triggers = [SemVerSigFig.prerelease] + all_triggers = {SemVerSigFig.prerelease} + + 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) From 5168fb78a46233ea595b941a11b0a35bac026b46 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 11 Apr 2019 10:11:57 +0100 Subject: [PATCH 20/28] add support for yaml/yml --- src/auto_version/config.py | 4 +++- src/auto_version/tests/test_autoversion.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/auto_version/config.py b/src/auto_version/config.py index 12f1452..8079ec2 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -56,7 +56,9 @@ class AutoVersionConfig(object): _forward_aliases = {} # autopopulated later - reverse mapping of the above targets = [os.path.join("src", "_version.py")] regexers = { - ".json": r"""^\s*[\"]?(?P[\w:]+)[\"]?\s*:[\t ]*[\"']?(?P((\\\")?[^\r\n\t\f\v\",](\\\")?)+)[\"']?,?""", # noqa + ".json": r"""^\s*[\"]?(?P[\w:]+)[\"]?\s*:[\t ]*[\"']?(?P((\\\"')?[^\r\n\t\f\v\",](\\\")?)+)[\"']?,?""", # noqa + ".yaml": r"""^\s*[\"']?(?P[\w]+)[\"']?\s*:\s*[\"']?(?P[\w\.\-\+]+)[\"']?""", # noqa + ".yml": r"""^\s*[\"']?(?P[\w]+)[\"']?\s*:\s*[\"']?(?P[\w\.\-\+]+)[\"']?""", # 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 diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 5f614cb..8850cb8 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -366,3 +366,13 @@ 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\r\n"""] + non_matching = [ + ' image: $CI_REGISTRY_IMAGE/versioning:$CI_COMMIT_REF_NAME\r\n', + """entrypoint: [""]\r\n""", + ] From 548df68151c5ea4863d8446d32014e21c5697312 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 11 Apr 2019 11:07:18 +0100 Subject: [PATCH 21/28] better yaml --- src/auto_version/config.py | 6 +++--- src/auto_version/tests/test_autoversion.py | 25 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/auto_version/config.py b/src/auto_version/config.py index 8079ec2..e606fdb 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -56,9 +56,9 @@ class AutoVersionConfig(object): _forward_aliases = {} # autopopulated later - reverse mapping of the above 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\.\-\+]+)[\"']?""", # noqa - ".yml": r"""^\s*[\"']?(?P[\w]+)[\"']?\s*:\s*[\"']?(?P[\w\.\-\+]+)[\"']?""", # noqa + ".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 diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 8850cb8..7b340f3 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -285,18 +285,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( @@ -373,6 +395,5 @@ class YamlRegexTest(BaseReplaceCheck): lines = [""" "custom_Key": '1.2.3.4+dev0'\r\n""", """ custom_Key: 1.2.3.4+dev0\r\n"""] non_matching = [ - ' image: $CI_REGISTRY_IMAGE/versioning:$CI_COMMIT_REF_NAME\r\n', - """entrypoint: [""]\r\n""", + """entrypoint: [""]\r\n""", # don't match on empty arrays ] From dea8a58c4abe795e3c161e0218b9e754dc555bd2 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 2 May 2019 17:51:54 +0100 Subject: [PATCH 22/28] don't perform replacement if line key isn't used --- src/auto_version/auto_version_tool.py | 16 ++++++-- src/auto_version/replacement_handler.py | 27 +++++++------ src/auto_version/tests/test_autoversion.py | 44 +++++++++++++++++----- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 333deb7..a528a87 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -45,8 +45,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 @@ -430,9 +434,13 @@ def main( # 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_version = utils.make_new_semver(current_semver, last_release_semver, triggers, **overrides) + new_version = utils.make_new_semver( + current_semver, last_release_semver, triggers, **overrides + ) - updates.update(get_finalised_updates(release_mode=release, version=str(new_version))) + updates.update( + get_finalised_updates(release_mode=release, version=str(new_version)) + ) # write out the individual parts of the version updates.update(new_version._asdict()) 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/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 7b340f3..91ae387 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -1,5 +1,4 @@ import contextlib -import semver import functools import imp import os @@ -8,6 +7,7 @@ import subprocess import unittest +import semver import six from auto_version import auto_version_tool from auto_version import utils @@ -151,9 +151,25 @@ def test_sigfig_compare_lt(self): 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"))) + 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): @@ -168,7 +184,11 @@ 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)) + str( + utils.make_new_semver( + semver.parse_version_info(current), previous, bumps + ) + ), ) def test_release_bump(self): @@ -392,8 +412,14 @@ class XMLRegexTest(BaseReplaceCheck): 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\r\n"""] - non_matching = [ - """entrypoint: [""]\r\n""", # don't match on empty arrays + 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 From a20b380ea4748bfa03b9e0d866e4f1f82808a96e Mon Sep 17 00:00:00 2001 From: David Hyman Date: Thu, 23 May 2019 14:53:38 +0100 Subject: [PATCH 23/28] 'ancestor' tag lookup is more accurate changes the cli --- docs/news/ancestor.major | 2 + src/auto_version/auto_version_tool.py | 69 ++++++++++++----- src/auto_version/cli.py | 5 +- src/auto_version/config.py | 11 ++- src/auto_version/tests/test_autoversion.py | 87 +++++++++++++++------- 5 files changed, 122 insertions(+), 52 deletions(-) create mode 100644 docs/news/ancestor.major diff --git a/docs/news/ancestor.major b/docs/news/ancestor.major new file mode 100644 index 0000000..48e5826 --- /dev/null +++ b/docs/news/ancestor.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/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index a528a87..3ae52bc 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -276,16 +276,15 @@ def get_dvcs_ordered_tag_semvers(): return ordered_versions -def get_dvcs_latest_tag_semver(): +def get_dvcs_repo_latest_version_semver(): + """Gets the most recent version across the whole repo""" ordered_versions = get_dvcs_ordered_tag_semvers() - result = None - if ordered_versions: - result = ordered_versions.pop() - _LOG.info("latest version found across all dvcs tags: %s", result) - return result + 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_previous_release_semver(): +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 @@ -293,17 +292,45 @@ def get_dvcs_previous_release_semver(): break else: version = None - _LOG.info("previous release found across all dvcs tags: %s", version) + _LOG.info("latest release found across all dvcs tags: %s", version) return version -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_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): @@ -323,12 +350,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 @@ -412,7 +441,7 @@ def main( load_config(config_path) all_data = {} - last_release_semver = incr_from_release and get_dvcs_previous_release_semver() + last_release_semver = incr_from_release and 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) diff --git a/src/auto_version/cli.py b/src/auto_version/cli.py index d66fbd9..2c4d095 100644 --- a/src/auto_version/cli.py +++ b/src/auto_version/cli.py @@ -73,9 +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 e606fdb..e67c2b0 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -25,9 +25,10 @@ class Constants(object): # source and destination control FROM_SOURCE = "source" - FROM_VCS_ANCESTOR = "vcs" - FROM_VCS_LATEST = "vcs-latest" - FROM_VCS_PREVIOUS_RELEASE = "vcs-previous-release" + FROM_VCS_PREVIOUS_VERSION = "vcs-prev-version" + FROM_VCS_PREVIOUS_RELEASE = "vcs-prev-release" + FROM_VCS_LATEST_VERSION = "vcs-global-latest" + FROM_VCS_LATEST_RELEASE = "vcs-global-release" TO_SOURCE = "source" TO_VCS = "vcs" @@ -72,7 +73,9 @@ class AutoVersionConfig(object): 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 + 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/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 91ae387..5091d3e 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -234,55 +234,90 @@ 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"), From eb5764fb5d380d0b927129bcecaac357a64f3c70 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Fri, 24 May 2019 10:30:20 +0100 Subject: [PATCH 24/28] match the 'incr_from_release' to the vcs mode (previous or latest) --- src/auto_version/auto_version_tool.py | 9 ++++++++- src/auto_version/config.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 3ae52bc..359a00d 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -441,7 +441,14 @@ def main( load_config(config_path) all_data = {} - last_release_semver = incr_from_release and get_dvcs_repo_latest_release_semver() + 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) diff --git a/src/auto_version/config.py b/src/auto_version/config.py index e67c2b0..e7c8175 100644 --- a/src/auto_version/config.py +++ b/src/auto_version/config.py @@ -27,7 +27,7 @@ class Constants(object): FROM_SOURCE = "source" FROM_VCS_PREVIOUS_VERSION = "vcs-prev-version" FROM_VCS_PREVIOUS_RELEASE = "vcs-prev-release" - FROM_VCS_LATEST_VERSION = "vcs-global-latest" + FROM_VCS_LATEST_VERSION = "vcs-global-version" FROM_VCS_LATEST_RELEASE = "vcs-global-release" TO_SOURCE = "source" TO_VCS = "vcs" From e719d54e53479348bd1f2e456b1a2de5625ad915 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Tue, 11 Jun 2019 11:46:45 +0100 Subject: [PATCH 25/28] 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. --- src/auto_version/auto_version_tool.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 359a00d..8a6feb8 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -185,20 +185,6 @@ def get_lock_behaviour(triggers, all_data, lock): return updates -def get_finalised_updates(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" @@ -474,9 +460,16 @@ def main( current_semver, last_release_semver, triggers, **overrides ) - updates.update( - get_finalised_updates(release_mode=release, version=str(new_version)) - ) + 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(new_version._asdict()) From 1b2d2a58431abf59cf1197545755de3c88000262 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Fri, 7 Aug 2020 16:36:06 +0100 Subject: [PATCH 26/28] 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. --- src/auto_version/auto_version_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index 8a6feb8..c0b97f0 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -198,7 +198,7 @@ def get_dvcs_info(): def get_all_versions_from_tags(tags): # build a regex from our version template - re_safe_placeholder = 10 * "v" + re_safe_placeholder = "THIS_IS_WHERE_THE_VERSION_GOES" tag_re = ( "^" + re.escape( From 06fefd238b01137ae88b51aba54766065d5577c1 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Fri, 7 Aug 2020 17:27:10 +0100 Subject: [PATCH 27/28] add tests and further improve the regex --- src/auto_version/auto_version_tool.py | 6 ++- src/auto_version/tests/test_autoversion.py | 45 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index c0b97f0..39a6351 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -197,13 +197,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 = "THIS_IS_WHERE_THE_VERSION_GOES" + 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) diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index 5091d3e..ab7e70d 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -12,6 +12,7 @@ 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 @@ -324,6 +325,50 @@ def test_to_tag(self): ) +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""" From 51a5a01db3af1ab904c2577198c1f1bcf90318c4 Mon Sep 17 00:00:00 2001 From: David Hyman Date: Wed, 21 Apr 2021 19:14:33 +0100 Subject: [PATCH 28/28] partial deps upgrade (no functional changes) to semver lib 3.x --- requirements.txt | 2 +- setup.py | 3 +- src/auto_version/auto_version_tool.py | 8 ++-- src/auto_version/replacement_handler.py | 4 +- src/auto_version/tests/test_autoversion.py | 52 +++++++++++++--------- src/auto_version/utils.py | 41 ++++++++++------- 6 files changed, 64 insertions(+), 46 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1dc4903..a8ed041 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ toml -semver~=2.13 +semver==3.0.0.dev2 six diff --git a/setup.py b/setup.py index c394cf1..ca84d81 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ import os -from setuptools import setup + from setuptools import find_packages +from setuptools import setup repository_dir = os.path.dirname(__file__) diff --git a/src/auto_version/auto_version_tool.py b/src/auto_version/auto_version_tool.py index ced2c10..3465bf2 100644 --- a/src/auto_version/auto_version_tool.py +++ b/src/auto_version/auto_version_tool.py @@ -449,7 +449,7 @@ def main( if set_to: _LOG.debug("setting version directly: %s", set_to) # parse it - validation failure will raise a ValueError - new_version = semver.parse_version_info(set_to) + new_version = semver.Version.parse(set_to) if not lock: warnings.warn( "After setting version manually, does it need locking for a CI flow, to avoid an extraneous increment?", @@ -463,8 +463,8 @@ def main( current_semver, last_release_semver, triggers, **overrides ) - release_string = semver.finalize_version(str(new_version)) - release_version = semver.parse_version_info(release_string) + release_version = new_version.finalize_version() + release_string = str(release_version) if release: new_version = release_version updates[Constants.RELEASE_FIELD] = config.RELEASED_VALUE @@ -475,7 +475,7 @@ def main( updates[Constants.VERSION_STRICT_FIELD] = release_string # write out the individual parts of the version - updates.update(new_version._asdict()) + updates.update(new_version.to_dict()) # only rewrite a field that the user has specified in the configuration source_file_updates = { diff --git a/src/auto_version/replacement_handler.py b/src/auto_version/replacement_handler.py index 5cba1cd..340a942 100644 --- a/src/auto_version/replacement_handler.py +++ b/src/auto_version/replacement_handler.py @@ -23,7 +23,9 @@ def __call__(self, match): """ original = match.string key = match.group(Constants.KEY_GROUP) - replacement = self.params[key] # if there's nothing in the lookup, raise KeyError + 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 diff --git a/src/auto_version/tests/test_autoversion.py b/src/auto_version/tests/test_autoversion.py index b9bec47..bfa41e2 100644 --- a/src/auto_version/tests/test_autoversion.py +++ b/src/auto_version/tests/test_autoversion.py @@ -1,10 +1,10 @@ import contextlib import functools -import imp import os import re import shlex import subprocess +import textwrap import unittest import semver @@ -115,8 +115,21 @@ def test_increment_existing_prerelease(self): def test_end_to_end(self): self.call(bump="major") filepath = os.path.join(os.path.dirname(__file__), "example.py") - example = imp.load_source("example", filepath) - self.assertEqual(example.VERSION, "20.0.0-dev.1") + with open(filepath) as fh: + content = fh.read() + self.assertEqual( + content, + textwrap.dedent( + """ + LOCK = False + RELEASE = True + VERSION = "20.0.0-dev.1" + VERSION_AGAIN = "20.0.0-dev.1" + STRICT_VERSION = "20.0.0" + UNRELATED_STRING = "apple" + """ + ).lstrip(), + ) def test_simple_config_bump(self): old, new, updates = self.call(config_path="simple.toml", bump="minor") @@ -164,9 +177,9 @@ def test_bump_patch(self): 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"))) + self.assertTrue(utils.is_release(semver.Version.parse("1.2.3"))) + self.assertFalse(utils.is_release(semver.Version.parse("1.2.3-RC.1"))) + self.assertFalse(utils.is_release(semver.Version.parse("1.2.3+abc"))) def test_sigfig_max(self): self.assertEqual("minor", utils.max_sigfig(["minor", "patch"])) @@ -188,20 +201,19 @@ 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") + semver.Version.parse("1.2.3"), semver.Version.parse("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"), + semver.Version.parse("1.2.3"), semver.Version.parse("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") + semver.Version.parse("1.2.3"), semver.Version.parse("1.2.3") ), ) @@ -215,14 +227,10 @@ def setUpClass(cls): 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 + previous = semver.Version.parse(previous) if previous else None self.assertEqual( expect, - str( - utils.make_new_semver( - semver.parse_version_info(current), previous, bumps - ) - ), + str(utils.make_new_semver(semver.Version.parse(current), previous, bumps)), ) def test_release_bump(self): @@ -289,7 +297,7 @@ def test_from_ancestor_version(self): { "VERSION": bumped, "VERSION_AGAIN": bumped, - "STRICT_VERSION": semver.finalize_version(bumped), + "STRICT_VERSION": str(semver.Version.parse(bumped).finalize_version()), }, ) @@ -303,7 +311,7 @@ def test_from_ancestor_release(self): { "VERSION": bumped, "VERSION_AGAIN": bumped, - "STRICT_VERSION": semver.finalize_version(bumped), + "STRICT_VERSION": str(semver.Version.parse(bumped).finalize_version()), }, ) @@ -315,7 +323,7 @@ def test_from_latest_of_all_time(self): { "VERSION": bumped, "VERSION_AGAIN": bumped, - "STRICT_VERSION": semver.finalize_version(bumped), + "STRICT_VERSION": str(semver.Version.parse(bumped).finalize_version()), }, ) @@ -327,7 +335,7 @@ def test_from_latest_of_all_time_release(self): { "VERSION": bumped, "VERSION_AGAIN": bumped, - "STRICT_VERSION": semver.finalize_version(bumped), + "STRICT_VERSION": str(semver.Version.parse(bumped).finalize_version()), }, ) @@ -348,12 +356,12 @@ def test_to_tag(self): { "VERSION": bumped, "VERSION_AGAIN": bumped, - "STRICT_VERSION": semver.finalize_version(bumped), + "STRICT_VERSION": str(semver.Version.parse(bumped).finalize_version()), }, ) version = auto_version_tool.get_dvcs_repo_latest_version_semver() self.assertEqual( - dict(version._asdict()), + dict(version.to_dict()), dict(major=5, minor=0, patch=0, build=None, prerelease="dev.1"), ) diff --git a/src/auto_version/utils.py b/src/auto_version/utils.py index 3a39d32..cd7b8cc 100644 --- a/src/auto_version/utils.py +++ b/src/auto_version/utils.py @@ -16,7 +16,7 @@ def from_text_or_none(text): """ if text is not None: try: - return semver.parse_version_info(text) + return semver.Version.parse(text) except ValueError: _LOG.debug("version string is not semver-compatible: %r", text) pass @@ -46,8 +46,12 @@ def get_semver_from_source(data): # we didn't have enough components pass - versions = [potential for potential in potentials if from_text_or_none(potential)] - release_versions = {semver.finalize_version(version) for version in versions} + actual_versions = [] + for potential in potentials: + version = from_text_or_none(potential) + if version: + actual_versions.append(version) + release_versions = {version.finalize_version() for version in actual_versions} if len(release_versions) > 1: raise ValueError( @@ -55,15 +59,15 @@ def get_semver_from_source(data): % (release_versions, known) ) - if not versions: + if not actual_versions: _LOG.debug("key pairs found: \n%r", known) raise ValueError("could not find existing semver") result = None - if versions: - result = versions[0] + if actual_versions: + result = actual_versions[0] _LOG.info("latest version found in source: %r", result) - return semver.parse_version_info(result) + return result def get_token_args(sig_fig): @@ -84,7 +88,9 @@ def max_sigfig(sigfigs): 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 + for sig_fig in reversed( + SemVerSigFig + ): # iterate sig figs in order of least significance if sig_fig in sigfigs: return sig_fig @@ -120,13 +126,15 @@ def make_new_semver(current_semver, last_release_semver, all_triggers, **overrid :param overrides: explicit values for some or all of the sigfigs :return: """ - version_string = str(current_semver) + proposed_version = 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)): + 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: @@ -142,23 +150,22 @@ def make_new_semver(current_semver, last_release_semver, all_triggers, **overrid 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) + proposed_version = getattr(current_semver, "bump_" + bump_sigfig)( + **get_token_args(bump_sigfig) ) 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 + proposed_version = proposed_version.bump_prerelease( + 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)) + setattr(proposed_version, "_" + k, prefix + str(v)) - return version_info + return proposed_version