Repository: pypa/setuptools_scm
Pull request #240 (2018-04-06 16:17:06)

add support for branch name based testing, add simplified semver support


Patch:
diff --git a/.gitignore b/.gitignore
index 19ae6c2..54ef3e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ htmlcov/
 .coverage
 .coverage.*
 .cache
+.pytest_cache
 nosetests.xml
 coverage.xml
 *,cover
diff --git a/setup.py b/setup.py
index e3203a9..7fc33f3 100644
--- a/setup.py
+++ b/setup.py
@@ -87,6 +87,7 @@ arguments = dict(
         [setuptools_scm.version_scheme]
         guess-next-dev = setuptools_scm.version:guess_next_dev_version
         post-release = setuptools_scm.version:postrelease_version
+        python-simplified-semver = setuptools_scm.version:simplified_semver_version
 
         [setuptools_scm.local_scheme]
         node-and-date = setuptools_scm.version:get_local_node_and_date
diff --git a/setuptools_scm/git.py b/setuptools_scm/git.py
index b399f67..6f35e78 100644
--- a/setuptools_scm/git.py
+++ b/setuptools_scm/git.py
@@ -39,6 +39,13 @@ class GitWorkdir(object):
         out, _, _ = self.do_ex("git status --porcelain --untracked-files=no")
         return bool(out)
 
+    def get_branch(self):
+        branch, err, ret = self.do_ex("git rev-parse --abbrev-ref HEAD")
+        if ret:
+            trace("branch err", branch, err, ret)
+            return
+        return branch
+
     def is_shallow(self):
         return isfile(join(self.path, '.git/shallow'))
 
@@ -89,7 +96,7 @@ def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
     if pre_parse:
         pre_parse(wd)
 
-    out, err, ret = do_ex(describe_command, root)
+    out, err, ret = wd.do_ex(describe_command)
     if ret:
         # If 'git describe' failed, try to get the information otherwise.
         rev_node = wd.node()
@@ -103,6 +110,7 @@ def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
             distance=wd.count_all_nodes(),
             node='g' + rev_node,
             dirty=dirty,
+            branch=wd.get_branch(),
         )
 
     # 'out' looks e.g. like 'v1.5.0-0-g4060507' or
@@ -115,10 +123,11 @@ def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
 
     tag, number, node = out.rsplit('-', 2)
     number = int(number)
+    branch = wd.get_branch()
     if number:
-        return meta(tag, distance=number, node=node, dirty=dirty)
+        return meta(tag, distance=number, node=node, dirty=dirty, branch=branch)
     else:
-        return meta(tag, node=node, dirty=dirty)
+        return meta(tag, node=node, dirty=dirty, branch=branch)
 
 
 def list_files_in_archive(path):
diff --git a/setuptools_scm/hg.py b/setuptools_scm/hg.py
index 22ab6a9..422e468 100644
--- a/setuptools_scm/hg.py
+++ b/setuptools_scm/hg.py
@@ -5,7 +5,7 @@ from .version import meta, tags_to_versions
 FILES_COMMAND = 'hg locate -I .'
 
 
-def _hg_tagdist_normalize_tagcommit(root, tag, dist, node):
+def _hg_tagdist_normalize_tagcommit(root, tag, dist, node, branch):
     dirty = node.endswith('+')
     node = 'h' + node.strip('+')
 
@@ -17,13 +17,14 @@ def _hg_tagdist_normalize_tagcommit(root, tag, dist, node):
               " and not tag({tag!r}))"  # ignore the tagged commit itself
               ).format(tag=tag)
     if tag != '0.0':
-        commits = do(['hg', 'log', '-r', revset, '--template', '{node|short}'],
+        commits = do(['hg', 'log', '-r', revset,
+                      '--template', '{node|short}'],
                      root)
     else:
         commits = True
     trace('normalize', locals())
     if commits or dirty:
-        return meta(tag, distance=dist, node=node, dirty=dirty)
+        return meta(tag, distance=dist, node=node, dirty=dirty, branch=branch)
     else:
         return meta(tag)
 
@@ -31,20 +32,21 @@ def _hg_tagdist_normalize_tagcommit(root, tag, dist, node):
 def parse(root):
     if not has_command('hg'):
         return
-    identity_data = do('hg id -i -t', root).split()
+    identity_data = do('hg id -i -b -t', root).split()
     if not identity_data:
         return
     node = identity_data.pop(0)
+    branch = identity_data.pop(0)
     tags = tags_to_versions(identity_data)
     # filter tip in degraded mode on old setuptools
     tags = [x for x in tags if x != 'tip']
     dirty = node[-1] == '+'
     if tags:
-        return meta(tags[0], dirty=dirty)
+        return meta(tags[0], dirty=dirty, branch=branch)
 
-    if node.strip('+') == '0'*12:
+    if node.strip('+') == '0' * 12:
         trace('initial node', root)
-        return meta('0.0', dirty=dirty)
+        return meta('0.0', dirty=dirty, branch=branch)
 
     try:
         tag = get_latest_normalizable_tag(root)
@@ -52,7 +54,7 @@ def parse(root):
         if tag == 'null':
             tag = '0.0'
             dist = int(dist) + 1
-        return _hg_tagdist_normalize_tagcommit(root, tag, dist, node)
+        return _hg_tagdist_normalize_tagcommit(root, tag, dist, node, branch)
     except ValueError:
         pass  # unpacking failed, old hg
 
diff --git a/setuptools_scm/version.py b/setuptools_scm/version.py
index 151f336..8c4ea7c 100644
--- a/setuptools_scm/version.py
+++ b/setuptools_scm/version.py
@@ -2,6 +2,8 @@ from __future__ import print_function
 import datetime
 import warnings
 import re
+from itertools import chain, repeat, islice
+
 from .utils import trace
 
 from pkg_resources import iter_entry_points
@@ -9,6 +11,15 @@ from pkg_resources import iter_entry_points
 from distutils import log
 from pkg_resources import parse_version
 
+SEMVER_MINOR = 2
+SEMVER_PATCH = 3
+SEMVER_LEN = 3
+
+
+def _pad(iterable, size, padding=None):
+    padded = chain(iterable, repeat(padding))
+    return list(islice(padded, size))
+
 
 def _get_version_class():
     modern_version = parse_version("1.0")
@@ -64,6 +75,7 @@ class ScmVersion(object):
     def __init__(self, tag_version,
                  distance=None, node=None, dirty=False,
                  preformatted=False,
+                 branch=None,
                  **kw):
         if kw:
             trace("unknown args", kw)
@@ -76,6 +88,7 @@ class ScmVersion(object):
         self.extra = kw
         self.dirty = dirty
         self.preformatted = preformatted
+        self.branch = branch
 
     @property
     def exact(self):
@@ -84,18 +97,23 @@ class ScmVersion(object):
     def __repr__(self):
         return self.format_with(
             '<ScmVersion {tag} d={distance}'
-            ' n={node} d={dirty} x={extra}>')
+            ' n={node} d={dirty} b={branch} x={extra}>')
 
     def format_with(self, fmt, **kw):
         return fmt.format(
             time=self.time,
             tag=self.tag, distance=self.distance,
-            node=self.node, dirty=self.dirty, extra=self.extra, **kw)
+            node=self.node, dirty=self.dirty, extra=self.extra,
+            branch=self.branch, **kw)
 
     def format_choice(self, clean_format, dirty_format, **kw):
         return self.format_with(
             dirty_format if self.dirty else clean_format, **kw)
 
+    def format_next_version(self, guess_next, fmt="{guessed}.dev{distance}", **kw):
+        guessed = guess_next(self.tag, **kw)
+        return self.format_with(fmt, guessed=guessed)
+
 
 def _parse_tag(tag, preformatted):
     if preformatted:
@@ -112,11 +130,9 @@ def meta(tag, distance=None, dirty=False, node=None, preformatted=False, **kw):
     return ScmVersion(tag, distance, node, dirty, preformatted, **kw)
 
 
-def guess_next_version(tag_version, distance):
+def guess_next_version(tag_version):
     version = _strip_local(str(tag_version))
-    bumped = _bump_dev(version) or _bump_regex(version)
-    suffix = '.dev%s' % distance
-    return bumped + suffix
+    return _bump_dev(version) or _bump_regex(version)
 
 
 def _strip_local(version_string):
@@ -134,7 +150,7 @@ def _bump_dev(version):
 
 
 def _bump_regex(version):
-    prefix, tail = re.match('(.*?)(\d+)$', version).groups()
+    prefix, tail = re.match(r'(.*?)(\d+)$', version).groups()
     return '%s%d' % (prefix, int(tail) + 1)
 
 
@@ -142,7 +158,29 @@ def guess_next_dev_version(version):
     if version.exact:
         return version.format_with("{tag}")
     else:
-        return guess_next_version(version.tag, version.distance)
+        return version.format_next_version(guess_next_version)
+
+
+def guess_next_simple_semver(version, retain, increment=True):
+    parts = map(int, str(version).split('.'))
+    parts = _pad(parts, retain, 0)
+    if increment:
+        parts[-1] += 1
+    parts = _pad(parts, SEMVER_LEN, 0)
+    return '.'.join(map(str, parts))
+
+
+def simplified_semver_version(version):
+    if version.exact:
+        return guess_next_simple_semver(
+            version.tag, retain=SEMVER_LEN, increment=False)
+    else:
+        if version.branch is not None and 'feature' in version.branch:
+            return version.format_next_version(
+                guess_next_simple_semver, retain=SEMVER_MINOR)
+        else:
+            return version.format_next_version(
+                guess_next_simple_semver, retain=SEMVER_PATCH)
 
 
 def _format_local_with_time(version, time_format):


Test patch:
diff --git a/testing/test_functions.py b/testing/test_functions.py
index c3cc86e..4198728 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -14,14 +14,14 @@ class MockTime(object):
 
 
 @pytest.mark.parametrize('tag, expected', [
-    ('1.1', '1.2.dev0'),
-    ('1.2.dev', '1.2.dev0'),
-    ('1.1a2', '1.1a3.dev0'),
-    ('23.24.post2+deadbeef', '23.24.post3.dev0'),
-    ])
+    ('1.1', '1.2'),
+    ('1.2.dev', '1.2'),
+    ('1.1a2', '1.1a3'),
+    ('23.24.post2+deadbeef', '23.24.post3'),
+])
 def test_next_tag(tag, expected):
     version = pkg_resources.parse_version(tag)
-    assert guess_next_version(version, 0) == expected
+    assert guess_next_version(version) == expected
 
 
 VERSIONS = {
diff --git a/testing/test_git.py b/testing/test_git.py
index e829fc8..5762452 100644
--- a/testing/test_git.py
+++ b/testing/test_git.py
@@ -99,7 +99,7 @@ def test_git_shallow_autocorrect(shallow_wd, recwarn):
 def test_find_files_stop_at_root_git(wd):
     wd.commit_testfile()
     wd.cwd.ensure('project/setup.cfg')
-    assert integration.find_files(str(wd.cwd/'project')) == []
+    assert integration.find_files(str(wd.cwd / 'project')) == []
 
 
 @pytest.mark.issue(128)
@@ -133,3 +133,13 @@ def test_git_archive_subdirectory(wd):
     wd('git add foobar')
     wd.commit()
     assert integration.find_files(str(wd.cwd)) == ['foobar/test1.txt']
+
+
+def test_git_feature_branch_increments_major(wd):
+    wd.commit_testfile()
+    wd("git tag 1.0.0")
+    wd.commit_testfile()
+    assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1")
+    wd("git checkout -b feature/fun")
+    wd.commit_testfile()
+    assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0")
diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py
index ab1a2d8..a054b75 100644
--- a/testing/test_mercurial.py
+++ b/testing/test_mercurial.py
@@ -18,10 +18,10 @@ archival_mapping = {
     '1.1.dev3+h000000000000': {
         'latesttag': '1.0',
         'latesttagdistance': '3',
-        'node': '0'*20,
+        'node': '0' * 20,
     },
     '0.0': {
-        'node': '0'*20,
+        'node': '0' * 20,
     },
     '1.2.2': {'tag': 'release-1.2.2'},
     '1.2.2.dev0': {'tag': 'release-1.2.2.dev'},
@@ -41,7 +41,7 @@ def test_archival_to_version(expected, data):
 def test_find_files_stop_at_root_hg(wd):
     wd.commit_testfile()
     wd.cwd.ensure('project/setup.cfg')
-    assert integration.find_files(str(wd.cwd/'project')) == []
+    assert integration.find_files(str(wd.cwd / 'project')) == []
 
 
 # XXX: better tests for tag prefixes
@@ -114,7 +114,7 @@ def test_parse_no_worktree(tmpdir):
 def version_1_0(wd):
     wd('hg branch default')
     wd.commit_testfile()
-    wd('hg tag 1.0 -u test -d "0 0"')
+    wd('hg tag 1.0.0 -u test -d "0 0"')
     return wd
 
 
@@ -131,14 +131,14 @@ def pre_merge_commit_after_tag(wd, version_1_0):
 
 @pytest.mark.usefixtures("pre_merge_commit_after_tag")
 def test_version_bump_before_merge_commit(wd):
-    assert wd.version.startswith('1.1.dev1+')
+    assert wd.version.startswith('1.0.1.dev1+')
 
 
 @pytest.mark.issue(219)
 @pytest.mark.usefixtures("pre_merge_commit_after_tag")
 def test_version_bump_from_merge_commit(wd):
     wd.commit()
-    assert wd.version.startswith('1.1.dev3+')  # issue 219
+    assert wd.version.startswith('1.0.1.dev3+')  # issue 219
 
 
 @pytest.mark.usefixtures("version_1_0")
@@ -149,9 +149,9 @@ def test_version_bump_from_commit_including_hgtag_mods(wd):
         tagfile.write('0  0\n')
     wd.write('branchfile', 'branchtext')
     wd(wd.add_command)
-    assert wd.version.startswith('1.1.dev1+')  # bump from dirty version
+    assert wd.version.startswith('1.0.1.dev1+')  # bump from dirty version
     wd.commit()  # commits both the testfile _and_ .hgtags
-    assert wd.version.startswith('1.1.dev2+')
+    assert wd.version.startswith('1.0.1.dev2+')
 
 
 @pytest.mark.issue(229)
@@ -161,4 +161,13 @@ def test_latest_tag_detection(wd):
     Note that will be superceded by the fix for pypa/setuptools_scm/issues/235
     """
     wd('hg tag some-random-tag')
-    assert wd.version == '1.0'
+    assert wd.version == '1.0.0'
+
+
+@pytest.mark.usefixtures("version_1_0")
+def test_feature_branch_increments_major(wd):
+
+    wd.commit_testfile()
+    assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.0.1")
+    wd("hg branch feature/fun")
+    assert wd.get_version(version_scheme="python-simplified-semver").startswith("1.1.0")
diff --git a/testing/test_version.py b/testing/test_version.py
new file mode 100644
index 0000000..7481c92
--- /dev/null
+++ b/testing/test_version.py
@@ -0,0 +1,23 @@
+import pytest
+from setuptools_scm.version import meta, simplified_semver_version
+
+
+@pytest.mark.parametrize('version, expected_next', [
+    pytest.param(meta("1.0.0"), "1.0.0", id='exact'),
+
+    pytest.param(meta("1.0"), "1.0.0", id='short_tag'),
+    pytest.param(meta("1.0.0", distance=2, branch='default'), "1.0.1.dev2",
+                 id='normal_branch'),
+
+    pytest.param(meta("1.0", distance=2, branch='default'), "1.0.1.dev2",
+                 id='normal_branch_short_tag'),
+    pytest.param(meta("1.0.0", distance=2, branch='feature'), "1.1.0.dev2",
+                 id='feature_branch'),
+    pytest.param(meta("1.0", distance=2, branch='feature'), "1.1.0.dev2",
+                 id='feature_branch_short_tag'),
+    pytest.param(meta("1.0.0", distance=2, branch='features/test'), "1.1.0.dev2",
+                 id='feature_in_branch'),
+])
+def test_next_semver(version, expected_next):
+    computed = simplified_semver_version(version)
+    assert computed == expected_next