diff --git a/setup.py b/setup.py index 2323750a..834a2d19 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,8 @@ def get_vcs_list(): return [filename for filename in os.listdir(project_path) - if path.isdir(path.join(project_path, filename))] + if path.isdir(path.join(project_path, filename)) + and filename != '__pycache__'] def generate_long_version_py(VCS): s = io.StringIO() @@ -71,7 +72,7 @@ def generate_versioneer_py(): s.write(get("src/subprocess_helper.py", do_strip=True)) for VCS in get_vcs_list(): - s.write(u("LONG_VERSION_PY['%s'] = '''\n" % VCS)) + s.write(u("LONG_VERSION_PY['%s'] = r'''\n" % VCS)) s.write(generate_long_version_py(VCS)) s.write(u("'''\n")) s.write(u("\n\n")) diff --git a/src/from_file.py b/src/from_file.py index 7f4eae05..fcbd78d7 100644 --- a/src/from_file.py +++ b/src/from_file.py @@ -47,4 +47,3 @@ def write_to_version_file(filename, versions): f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) - diff --git a/src/get_versions.py b/src/get_versions.py index 01553710..31cdb05f 100644 --- a/src/get_versions.py +++ b/src/get_versions.py @@ -7,6 +7,7 @@ def render(): pass # --STRIP DURING BUILD HANDLERS = {} # --STRIP DURING BUILD class NotThisMethod(Exception): pass # --STRIP DURING BUILD + class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" diff --git a/src/git/from_keywords.py b/src/git/from_keywords.py index ea7c040f..385c931f 100644 --- a/src/git/from_keywords.py +++ b/src/git/from_keywords.py @@ -79,12 +79,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, - "date": date} + "date": date, "branch": None} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - + "dirty": False, "error": "no suitable tags", "date": None, + "branch": None} diff --git a/src/git/from_vcs.py b/src/git/from_vcs.py index c7db10d1..07410b9a 100644 --- a/src/git/from_vcs.py +++ b/src/git/from_vcs.py @@ -97,6 +97,30 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): cwd=root) pieces["distance"] = int(count_out) # total number of commits + # abbrev-ref available with git >= 1.7 + branch_name_out, rc = run_command(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + branch_name = branch_name_out.strip() + if branch_name == 'HEAD': + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches_out, rc = run_command(GITS, ["branch", "--contains"], + cwd=root) + branches = branches_out.split('\n') + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches + if branch and branch[4:5] != '('] + if 'master' in branches: + branch_name = 'master' + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces['branch'] = branch_name + # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() diff --git a/src/git/long_get_versions.py b/src/git/long_get_versions.py index af081a5a..d5497236 100644 --- a/src/git/long_get_versions.py +++ b/src/git/long_get_versions.py @@ -7,6 +7,7 @@ def versions_from_parentdir(): pass # --STRIP DURING BUILD class NotThisMethod(Exception): pass # --STRIP DURING BUILD def render(): pass # --STRIP DURING BUILD + def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have diff --git a/src/render.py b/src/render.py index 9770d0ea..e8db4437 100644 --- a/src/render.py +++ b/src/render.py @@ -1,3 +1,5 @@ +import re # --STRIP DURING BUILD + def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" @@ -136,6 +138,46 @@ def render_git_describe_long(pieces): return rendered +def render_pep440_branch_based(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.BRANCH_gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.BRANCH_gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.BRANCH_gHEX[.dirty] + """ + replacements = ([' ', '.'], ['(', ''], [')', ''], ['\\', '.'], ['/', '.']) + branch_name = pieces.get('branch') or '' + if branch_name: + for old, new in replacements: + branch_name = branch_name.replace(old, new) + else: + branch_name = 'unknown_branch' + + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += '.dev0' + plus_or_dot(pieces) + rendered += "%d.%s.g%s" % ( + pieces["distance"], + branch_name, + pieces['short'] + ) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.%s.g%s" % ( + pieces["distance"], + branch_name, + pieces['short'] + ) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: @@ -156,6 +198,8 @@ def render(pieces, style): rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) + elif style == "pep440-branch-based": + rendered = render_pep440_branch_based(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": @@ -166,4 +210,3 @@ def render(pieces, style): return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} - diff --git a/src/setupfunc.py b/src/setupfunc.py index 69805c28..7d973d67 100644 --- a/src/setupfunc.py +++ b/src/setupfunc.py @@ -1,6 +1,6 @@ from __future__ import print_function # --STRIP DURING BUILD -import os, sys # --STRIP DURING BUILD +import os, re, sys # --STRIP DURING BUILD def get_root(): pass # --STRIP DURING BUILD def get_config_from_root(): pass # --STRIP DURING BUILD LONG_VERSION_PY = {} # --STRIP DURING BUILD @@ -50,6 +50,13 @@ def do_vcs_install(): pass # --STRIP DURING BUILD del get_versions """ +INIT_PY_SNIPPET_RE = re.compile(INIT_PY_SNIPPET.replace( + '(', r'\(').replace( + ')', r'\)').replace( + '[', r'\[').replace( + ']', r'\]').replace( + ' ._version', r' ([\w\._]+)?._version'), re.MULTILINE) + def do_setup(): """Do main VCS-independent setup function for installing Versioneer.""" @@ -84,7 +91,7 @@ def do_setup(): old = f.read() except EnvironmentError: old = "" - if INIT_PY_SNIPPET not in old: + if INIT_PY_SNIPPET_RE.search(old) is None: print(" appending to %s" % ipy) with open(ipy, "a") as f: f.write(INIT_PY_SNIPPET) @@ -142,7 +149,7 @@ def scan_setup_py(): for line in f.readlines(): if "import versioneer" in line: found.add("import") - if "versioneer.get_cmdclass()" in line: + if "versioneer.get_cmdclass(" in line: found.add("cmdclass") if "versioneer.get_version()" in line: found.add("get_version") diff --git a/test/git/test_git.py b/test/git/test_git.py index 41f2dfd3..a4e1d7fd 100644 --- a/test/git/test_git.py +++ b/test/git/test_git.py @@ -1,11 +1,12 @@ #! /usr/bin/python from __future__ import print_function -import os, sys +import os import shutil import tarfile import unittest import tempfile +import sys from pkg_resources import parse_version, SetuptoolsLegacyVersion @@ -16,14 +17,15 @@ from git import from_vcs, from_keywords from subprocess_helper import run_command -class ParseGitDescribe(unittest.TestCase): + +class Test_ParseGitDescribe(unittest.TestCase): def setUp(self): self.fakeroot = tempfile.mkdtemp() self.fakegit = os.path.join(self.fakeroot, ".git") os.mkdir(self.fakegit) def test_pieces(self): - def pv(git_describe, do_error=False, expect_pieces=False): + def pv(git_describe, do_error=False, expect_pieces=False, branch_name="HEAD"): def fake_run_command(exes, args, cwd=None, hide_stderr=None): if args[0] == "describe": if do_error == "describe": @@ -32,11 +34,16 @@ def fake_run_command(exes, args, cwd=None, hide_stderr=None): if args[0] == "rev-parse": if do_error == "rev-parse": return None, 0 - return "longlong\n", 0 + if args[1] == '--abbrev-ref': + return '%s\n' % branch_name, 0 + else: + return "longlong\n", 0 if args[0] == "rev-list": return "42\n", 0 if args[0] == "show": return "12345\n", 0 + elif args[0:2] == ["branch", "--contains"]: + return '', 0 self.fail("git called in weird way: %s" % (args,)) return from_vcs.git_pieces_from_vcs( "v", self.fakeroot, verbose=False, @@ -46,37 +53,50 @@ def fake_run_command(exes, args, cwd=None, hide_stderr=None): self.assertRaises(from_vcs.NotThisMethod, pv, "ignored", do_error="rev-parse") self.assertEqual(pv("1f"), - {"closest-tag": None, "dirty": False, "error": None, + {"branch": None, "closest-tag": None, + "dirty": False, "error": None, "distance": 42, "long": "longlong", "short": "longlon", "date": "12345"}) self.assertEqual(pv("1f-dirty"), - {"closest-tag": None, "dirty": True, "error": None, + {"branch": None, "closest-tag": None, + "dirty": True, "error": None, "distance": 42, "long": "longlong", "short": "longlon", "date": "12345"}) self.assertEqual(pv("v1.0-0-g1f"), - {"closest-tag": "1.0", "dirty": False, "error": None, + {"branch": None, "closest-tag": "1.0", + "dirty": False, "error": None, "distance": 0, "long": "longlong", "short": "1f", "date": "12345"}) self.assertEqual(pv("v1.0-0-g1f-dirty"), - {"closest-tag": "1.0", "dirty": True, "error": None, + {"branch": None, "closest-tag": "1.0", + "dirty": True, "error": None, "distance": 0, "long": "longlong", "short": "1f", "date": "12345"}) self.assertEqual(pv("v1.0-1-g1f"), - {"closest-tag": "1.0", "dirty": False, "error": None, + {"branch": None, "closest-tag": "1.0", + "dirty": False, "error": None, "distance": 1, "long": "longlong", "short": "1f", "date": "12345"}) self.assertEqual(pv("v1.0-1-g1f-dirty"), - {"closest-tag": "1.0", "dirty": True, "error": None, + {"branch": None, "closest-tag": "1.0", + "dirty": True, "error": None, + "distance": 1, + "long": "longlong", + "short": "1f", + "date": "12345"}) + self.assertEqual(pv("v1.0-1-g1f-dirty", branch_name="v1.0.x"), + {"branch": 'v1.0.x', "closest-tag": "1.0", + "dirty": True, "error": None, "distance": 1, "long": "longlong", "short": "1f", @@ -87,13 +107,14 @@ def tearDown(self): os.rmdir(self.fakeroot) -class Keywords(unittest.TestCase): +class Test_Keywords(unittest.TestCase): def parse(self, refnames, full, prefix=""): return from_keywords.git_versions_from_keywords( {"refnames": refnames, "full": full}, prefix, False) def test_parse(self): v = self.parse(" (HEAD, 2.0,master , otherbranch ) ", " full ") + self.assertEqual(v["branch"], None) self.assertEqual(v["version"], "2.0") self.assertEqual(v["full-revisionid"], "full") self.assertEqual(v["dirty"], False) @@ -101,6 +122,7 @@ def test_parse(self): def test_prefer_short(self): v = self.parse(" (HEAD, 2.0rc1, 2.0, 2.0rc2) ", " full ") + self.assertEqual(v["branch"], None) self.assertEqual(v["version"], "2.0") self.assertEqual(v["full-revisionid"], "full") self.assertEqual(v["dirty"], False) @@ -108,6 +130,7 @@ def test_prefer_short(self): def test_prefix(self): v = self.parse(" (HEAD, projectname-2.0) ", " full ", "projectname-") + self.assertEqual(v["branch"], None) self.assertEqual(v["version"], "2.0") self.assertEqual(v["full-revisionid"], "full") self.assertEqual(v["dirty"], False) @@ -119,6 +142,7 @@ def test_unexpanded(self): def test_no_tags(self): v = self.parse("(HEAD, master)", "full") + self.assertEqual(v["branch"], None) self.assertEqual(v["version"], "0+unknown") self.assertEqual(v["full-revisionid"], "full") self.assertEqual(v["dirty"], False) @@ -126,11 +150,28 @@ def test_no_tags(self): def test_no_prefix(self): v = self.parse("(HEAD, master, 1.23)", "full", "missingprefix-") + self.assertEqual(v["branch"], None) self.assertEqual(v["version"], "0+unknown") - self.assertEqual(v["full-revisionid"], "full") self.assertEqual(v["dirty"], False) self.assertEqual(v["error"], "no suitable tags") + def test_branch_heuristics(self): + v = self.parse("(v0.12.x)", "full", "v") + self.assertEqual(v["branch"], None) + # Questionable whether this is desirable. + self.assertEqual(v["version"], "0.12.x") + self.assertEqual(v["dirty"], False) + self.assertEqual(v["error"], None) + + def test_new_tag_style(self): + v = self.parse("(tag: v0.12.0)", "full", "v") + self.assertEqual(v["branch"], None) + self.assertEqual(v["version"], "0.12.0") + self.assertEqual(v["full-revisionid"], "full") + self.assertEqual(v["dirty"], False) + self.assertEqual(v["error"], None) + + expected_renders = """ closest-tag: 1.0 distance: 0 @@ -216,7 +257,8 @@ def test_no_prefix(self): """ -class RenderPieces(unittest.TestCase): + +class Test_RenderPieces(unittest.TestCase): def do_render(self, pieces): out = {} for style in ["pep440", "pep440-pre", "pep440-post", "pep440-old", @@ -271,7 +313,8 @@ def test_render(self): VERBOSE = False -class Repo(common.Common, unittest.TestCase): + +class Test_RepoIntegration(common.Common, unittest.TestCase): # There are three tree states we're interested in: # S1: sitting on the initial commit, no tags @@ -301,7 +344,7 @@ class Repo(common.Common, unittest.TestCase): # or test/demoapp-script-only/) def test_full(self): - self.run_test("test/demoapp", False, ".") + self.run_case("test/demoapp", False, ".") def test_script_only(self): # This test looks at an application that consists entirely of a @@ -310,19 +353,14 @@ def test_script_only(self): # anything executable. So of the 3 runtime situations examined by # Repo.test_full above, we only care about RB. (RA1 is valid too, but # covered by Repo). - self.run_test("test/demoapp-script-only", True, ".") + self.run_case("test/demoapp-script-only", True, ".") def test_project_in_subdir(self): # This test sets of the git repository so that the python project -- # i.e. setup.py -- is not located in the root directory - self.run_test("test/demoapp", False, "project") - - def run_test(self, demoapp_dir, script_only, project_sub_dir): - # The test dir should live under /tmp/ or /var/ or somewhere that - # isn't the child of the versioneer repo's .git directory, since that - # will confuse the tests that check what happens when there is no - # .git parent. So if you change this to use a fixed directory (say, - # when debugging problems), use /tmp/_test rather than ./_test . + self.run_case("test/demoapp", False, "project") + + def run_case(self, demoapp_dir, script_only, project_sub_dir): self.testdir = tempfile.mkdtemp() if VERBOSE: print("testdir: %s" % (self.testdir,)) if os.path.exists(self.testdir): @@ -597,6 +635,147 @@ def assertPEP440(self, got, state, tree, runtime): "%s: '%s' pep440-normalized to '%s'" % (where, got, str(pv))) + +class Test_GitRepo(common.Common, unittest.TestCase): + expecteds = {'S1': None, + 'S2': {'branch': 'master', + 'closest-tag': None, + 'dirty': False, + 'distance': 1, + 'error': None}, + 'S3': {'branch': 'master', + 'closest-tag': '1.0', + 'dirty': False, + 'distance': 0, + 'error': None}, + 'S4': {'branch': 'master', + 'closest-tag': '1.0', + 'dirty': False, + 'distance': 1, + 'error': None}, + 'S5': {'branch': 'master', + 'closest-tag': '2.0', + 'dirty': False, + 'distance': 0, + 'error': None}, + 'S6': {'branch': 'v1.x', + 'closest-tag': '1.0', + 'dirty': False, + 'distance': 1, + 'error': None}, + 'S7': {'branch': 'v1.x', + 'closest-tag': '1.1', + 'dirty': False, + 'distance': 0, + 'error': None}, + 'S8': {'branch': 'master', + 'closest-tag': '2.0', + 'dirty': False, + 'distance': 2, + 'error': None}, + 'S9': {'branch': 'master', + 'closest-tag': '2.1', + 'dirty': False, + 'distance': 0, + 'error': None}, + } + + def assert_case(self, case_name, dirty=False): + pieces = from_vcs.git_pieces_from_vcs(self.tag_prefix, self.gitdir, + verbose=False, + run_command=run_command) + pieces.pop('short') + pieces.pop('long') + pieces.pop('date') + expected = self.expecteds.get(case_name, {}) + + if dirty: + expected['dirty'] = True + if expected != pieces: + print('Case: %s' % case_name) + + self.assertEqual(expected, pieces) + + def write_file(self, fname, content): + with open(os.path.join(self.gitdir, fname), 'w') as fh: + fh.write(content) + + + def tearDown(self): + if os.path.exists(self.gitdir): + import shutil + shutil.rmtree(self.gitdir) + + def test(self): + self.tag_prefix = 'v' + + self.testdir = os.path.join(os.path.abspath(os.path.dirname(__file__)), + 'git_test_repo') + self.gitdir = os.path.join(self.testdir, 'demoapp') + + # Cleanup + self.tearDown() + os.makedirs(self.gitdir) + + # S1 + self.git("init") + self.assertRaises(from_vcs.NotThisMethod, + from_vcs.git_pieces_from_vcs, + self.tag_prefix, self.gitdir, run_command=run_command, + verbose=False) + + # S2 + self.write_file('a.txt', 'abc') + self.git("add", "a.txt") + self.git("commit", "-m", "First commmit.") + self.assert_case('S2') + + # S3 + self.git("tag", "-a", "v1.0", "-m", "First tag.") + self.git("branch", "v1.x") + self.assert_case('S3') + + # S4 + self.write_file('b.txt', 'abc') + self.git("add", "b.txt") + self.assert_case('S3', dirty=True) + self.git("commit", "-m", "Start of 2.0.") + self.assert_case('S4') + + # S5 + self.git("tag", "-a", "v2.0", "-m", "Second tag.") + self.assert_case('S5') + # Even checking out the SHA, without a branch, should figure out the + # appropriate branch name. + sha = self.git("rev-parse", "HEAD") + self.git("checkout", sha) + self.assert_case('S5') + + # S6 + self.git("checkout", "v1.x") + self.write_file('a.txt', 'abcdef') + self.git("commit", "-am", "Modify 1.x") + self.assert_case('S6') + + # S7 + self.git("tag", "-a", "v1.1", "-m", "Tag v1.1.") + self.assert_case('S7') + + # S8 + self.git("checkout", "master") + # Just check that S5 hasn't been distrupted - we've just added a tag. + self.assert_case('S5') + self.git("merge", "v1.x") + self.assert_case('S8') + + # S9 + self.git("tag", "-a", "v2.1", "-m", "Tag v2.1.") + self.assert_case('S9') + self.git("checkout", "v1.x") + # Again, just check that we haven't disrupted the v1.x branch. + self.assert_case('S7') + + if __name__ == '__main__': ver, rc = run_command(common.GITS, ["--version"], ".", True) print("git --version: %s" % ver.strip()) diff --git a/test/test_render.py b/test/test_render.py index ab01a936..23161b7b 100644 --- a/test/test_render.py +++ b/test/test_render.py @@ -1,9 +1,156 @@ +import os +import sys import unittest +SOURCE_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +if not os.path.exists(os.path.join(SOURCE_ROOT, 'versioneer.py')): + print('Please run "python setup.py make_versioneer"') + sys.exit(1) + +sys.path.insert(0, SOURCE_ROOT) from versioneer import render -class Testing_renderer_case_mixin(object): +expected_renders = """ +closest-tag: 1.0 +distance: 0 +dirty: False +pep440: 1.0 +pep440-pre: 1.0 +pep440-post: 1.0 +pep440-old: 1.0 +git-describe: 1.0 +git-describe-long: 1.0-0-g250b7ca + +closest-tag: 1.0 +distance: 0 +dirty: True +pep440: 1.0+0.g250b7ca.dirty +pep440-pre: 1.0 +pep440-post: 1.0.post0.dev0+g250b7ca +pep440-old: 1.0.post0.dev0 +git-describe: 1.0-dirty +git-describe-long: 1.0-0-g250b7ca-dirty + +closest-tag: 1.0 +distance: 1 +dirty: False +pep440: 1.0+1.g250b7ca +pep440-pre: 1.0.post.dev1 +pep440-post: 1.0.post1+g250b7ca +pep440-old: 1.0.post1 +git-describe: 1.0-1-g250b7ca +git-describe-long: 1.0-1-g250b7ca + +closest-tag: 1.0 +distance: 1 +dirty: True +pep440: 1.0+1.g250b7ca.dirty +pep440-pre: 1.0.post.dev1 +pep440-post: 1.0.post1.dev0+g250b7ca +pep440-old: 1.0.post1.dev0 +git-describe: 1.0-1-g250b7ca-dirty +git-describe-long: 1.0-1-g250b7ca-dirty + + +closest-tag: 1.0+plus +distance: 1 +dirty: False +pep440: 1.0+plus.1.g250b7ca +pep440-pre: 1.0+plus.post.dev1 +pep440-post: 1.0+plus.post1.g250b7ca +pep440-old: 1.0+plus.post1 +git-describe: 1.0+plus-1-g250b7ca +git-describe-long: 1.0+plus-1-g250b7ca + +closest-tag: 1.0+plus +distance: 1 +dirty: True +pep440: 1.0+plus.1.g250b7ca.dirty +pep440-pre: 1.0+plus.post.dev1 +pep440-post: 1.0+plus.post1.dev0.g250b7ca +pep440-old: 1.0+plus.post1.dev0 +git-describe: 1.0+plus-1-g250b7ca-dirty +git-describe-long: 1.0+plus-1-g250b7ca-dirty + + +closest-tag: None +distance: 1 +dirty: False +pep440: 0+untagged.1.g250b7ca +pep440-pre: 0.post.dev1 +pep440-post: 0.post1+g250b7ca +pep440-old: 0.post1 +git-describe: 250b7ca +git-describe-long: 250b7ca + +closest-tag: None +distance: 1 +dirty: True +pep440: 0+untagged.1.g250b7ca.dirty +pep440-pre: 0.post.dev1 +pep440-post: 0.post1.dev0+g250b7ca +pep440-old: 0.post1.dev0 +git-describe: 250b7ca-dirty +git-describe-long: 250b7ca-dirty + +""" + + +class Test_RenderPieces(unittest.TestCase): + def do_render(self, pieces): + out = {} + for style in ["pep440", "pep440-pre", "pep440-post", "pep440-old", + "git-describe", "git-describe-long"]: + out[style] = render(pieces, style)["version"] + DEFAULT = "pep440" + self.assertEqual(render(pieces, ""), render(pieces, DEFAULT)) + self.assertEqual(render(pieces, "default"), render(pieces, DEFAULT)) + return out + + def parse_expected(self): + base_pieces = {"long": "250b7ca731388d8f016db2e06ab1d6289486424b", + "short": "250b7ca", + "error": None} + more_pieces = {} + expected = {} + for line in expected_renders.splitlines(): + line = line.strip() + if not line: + if more_pieces and expected: + pieces = base_pieces.copy() + pieces.update(more_pieces) + yield (pieces, expected) + more_pieces = {} + expected = {} + continue + name, value = line.split(":") + name = name.strip() + value = value.strip() + if name == "distance": + more_pieces["distance"] = int(value) + elif name == "dirty": + more_pieces["dirty"] = bool(value.lower() == "true") + elif name == "closest-tag": + more_pieces["closest-tag"] = value + if value == "None": + more_pieces["closest-tag"] = None + else: + expected[name] = value + if more_pieces and expected: + pieces = base_pieces.copy() + pieces.update(more_pieces) + yield (pieces, expected) + + def test_render(self): + for (pieces, expected) in self.parse_expected(): + got = self.do_render(pieces) + for key in expected: + self.assertEqual(got[key], expected[key], + (pieces, key, got[key], expected[key])) + + +class renderer_case_mixin(object): """ This is a mixin object which can be combined with a unittest.TestCase which defines a style and an expected dictionary. See Test_pep440 for @@ -11,13 +158,20 @@ class Testing_renderer_case_mixin(object): """ def define_pieces(self, closest_tag, distance=0, dirty=False): + branch = getattr(self, 'branch', 'master') + if branch: + replacements = ([' ', '.'], ['(', ''], [')', ''], ['\\', '-'], ['/', '-']) + for old, new in replacements: + branch = branch.replace(old, new) + return {"error": '', "closest-tag": closest_tag, "distance": distance, "dirty": dirty, "short": "abc" if distance else '', "long": "abcdefg" if distance else '', - "date": "2016-05-31T13:02:11+0200"} + "date": "2016-05-31T13:02:11+0200", + "branch": branch} def assert_rendered(self, pieces, test_case_name): version = render(pieces, self.style)['version'] @@ -67,7 +221,7 @@ def test_error_getting_parts(self): 'error_getting_parts') -class Test_pep440(unittest.TestCase, Testing_renderer_case_mixin): +class Test_pep440(unittest.TestCase, renderer_case_mixin): style = 'pep440' expected = {'tagged_0_commits_clean': 'v1.2.3', 'tagged_0_commits_dirty': 'v1.2.3+0.g.dirty', @@ -81,7 +235,7 @@ class Test_pep440(unittest.TestCase, Testing_renderer_case_mixin): } -class Test_pep440_old(unittest.TestCase, Testing_renderer_case_mixin): +class Test_pep440_old(unittest.TestCase, renderer_case_mixin): style = 'pep440-old' expected = {'tagged_0_commits_clean': 'v1.2.3', 'tagged_0_commits_dirty': 'v1.2.3.post0.dev0', @@ -95,7 +249,7 @@ class Test_pep440_old(unittest.TestCase, Testing_renderer_case_mixin): } -class Test_pep440_post(unittest.TestCase, Testing_renderer_case_mixin): +class Test_pep440_post(unittest.TestCase, renderer_case_mixin): style = 'pep440-post' expected = {'tagged_0_commits_clean': 'v1.2.3', 'tagged_0_commits_dirty': 'v1.2.3.post0.dev0+g', @@ -109,7 +263,7 @@ class Test_pep440_post(unittest.TestCase, Testing_renderer_case_mixin): } -class Test_pep440_pre(unittest.TestCase, Testing_renderer_case_mixin): +class Test_pep440_pre(unittest.TestCase, renderer_case_mixin): style = 'pep440-pre' expected = {'tagged_0_commits_clean': 'v1.2.3', 'tagged_0_commits_dirty': 'v1.2.3', @@ -123,7 +277,7 @@ class Test_pep440_pre(unittest.TestCase, Testing_renderer_case_mixin): } -class Test_git_describe(unittest.TestCase, Testing_renderer_case_mixin): +class Test_git_describe(unittest.TestCase, renderer_case_mixin): style = 'git-describe' expected = {'tagged_0_commits_clean': 'v1.2.3', 'tagged_0_commits_dirty': 'v1.2.3-dirty', @@ -137,5 +291,133 @@ class Test_git_describe(unittest.TestCase, Testing_renderer_case_mixin): } +class Test_pep440_branch_based__master(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'master' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.master.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.master.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.master.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.master.g', + 'untagged_0_commits_dirty': '0+untagged.0.master.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.master.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.master.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__maint(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'v1.2.x' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.v1.2.x.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.v1.2.x.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.v1.2.x.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.v1.2.x.g', + 'untagged_0_commits_dirty': '0+untagged.0.v1.2.x.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.v1.2.x.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.v1.2.x.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__feature_branch(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'feature_branch' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.feature_branch.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.feature_branch.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.feature_branch.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.feature_branch.g', + 'untagged_0_commits_dirty': '0+untagged.0.feature_branch.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.feature_branch.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.feature_branch.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__no_branch_info(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = None + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.unknown_branch.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.unknown_branch.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.unknown_branch.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.unknown_branch.g', + 'untagged_0_commits_dirty': '0+untagged.0.unknown_branch.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.unknown_branch.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.unknown_branch.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__space_in_branch(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'foo bar' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.foo.bar.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.foo.bar.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.foo.bar.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.foo.bar.g', + 'untagged_0_commits_dirty': '0+untagged.0.foo.bar.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.foo.bar.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.foo.bar.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__forward_slash_in_branch(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'foo/bar' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.foo.bar.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.foo.bar.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.foo.bar.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.foo.bar.g', + 'untagged_0_commits_dirty': '0+untagged.0.foo.bar.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.foo.bar.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.foo.bar.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__back_slash_in_branch(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'foo\\bar' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.foo.bar.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.foo.bar.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.foo.bar.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.foo.bar.g', + 'untagged_0_commits_dirty': '0+untagged.0.foo.bar.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.foo.bar.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.foo.bar.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + +class Test_pep440_branch_based__parenthesis_in_branch(unittest.TestCase, + renderer_case_mixin): + style = 'pep440-branch-based' + branch = 'foo(bar)' + expected = {'tagged_0_commits_clean': 'v1.2.3', + 'tagged_0_commits_dirty': 'v1.2.3.dev0+0.foobar.g.dirty', + 'tagged_1_commits_clean': 'v1.2.3.dev0+1.foobar.gabc', + 'tagged_1_commits_dirty': 'v1.2.3.dev0+1.foobar.gabc.dirty', + 'untagged_0_commits_clean': '0+untagged.0.foobar.g', + 'untagged_0_commits_dirty': '0+untagged.0.foobar.g.dirty', + 'untagged_1_commits_clean': '0+untagged.1.foobar.gabc', + 'untagged_1_commits_dirty': '0+untagged.1.foobar.gabc.dirty', + 'error_getting_parts': 'unknown' + } + + if __name__ == '__main__': unittest.main()