10000 Merge branch 'pep440'. Closes #67. · drinkingjava/python-versioneer@c0d7588 · GitHub
[go: up one dir, main page]

Skip to content

Commit c0d7588

Browse files
committed
Merge branch 'pep440'. Closes python-versioneer#67.
2 parents 8f114cd + 1f2e68c commit c0d7588

File tree

6 files changed

+138
-48
lines changed

6 files changed

+138
-48
lines changed

README.md

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -197,17 +197,19 @@ import the top-level `versioneer.py` and run `get_versions()`.
197197
Both functions return a dictionary with different keys for different flavors
198198
of the version string:
199199

200-
* `['version']`: condensed tag+distance+shortid+dirty identifier. For git,
201-
this uses the output of `git describe --tags --dirty --always` but strips
202-
the tag_prefix. For example "0.11-2-g1076c97-dirty" indicates that the tree
203-
is like the "1076c97" commit but has uncommitted changes ("-dirty"), and
204-
that this commit is two revisions ("-2-") beyond the "0.11" tag. For
205-
released software (exactly equal to a known tag), the identifier will only
206-
contain the stripped tag, e.g. "0.11".
200+
* `['version']`: A condensed PEP440-compliant string, equal to the
201+
un-prefixed tag name for actual releases, and containing an additional
202+
"local version" section with more detail for in-between builds. For Git,
203+
this is TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe
204+
--tags --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates
205+
that the tree is like the "1076c97" commit but has uncommitted changes
206+
(".dirty"), and that this commit is two revisions ("+2") beyond the "0.11"
207+
tag. For released software (exactly equal to a known tag), the identifier
208+
will only contain the stripped tag, e.g. "0.11".
207209

208210
* `['full']`: detailed revision identifier. For Git, this is the full SHA1
209-
commit id, followed by "-dirty" if the tree contains uncommitted changes,
210-
e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac-dirty".
211+
commit id, followed by ".dirty" if the tree contains uncommitted changes,
212+
e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac.dirty".
211213

212214
Some variants are more useful than others. Including `full` in a bug report
213215
should allow developers to reconstruct the exact code being tested (or
@@ -216,13 +218,6 @@ developers). `version` is suitable for display in an "about" box or a CLI
216218
`--version` output: it can be easily compared against release notes and lists
217219
of bugs fixed in various releases.
218220

219-
In the future, this will also include a
220-
[PEP-0440](http://legacy.python.org/dev/peps/pep-0440/) -compatible flavor
221-
(e.g. `1.2.post0.dev123`). This loses a lot of information (and has no room
222-
for a hash-based revision id), but is safe to use in a `setup.py`
223-
"`version=`" argument. It also enables tools like *pip* to compare version
224-
strings and evaluate compatibility constraint declarations.
225-
226221
The `setup.py versioneer` command adds the following text to your
227222
`__init__.py` to place a basic version in `YOURPROJECT.__version__`:
228223

src/from_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def get_versions(default={}, verbose=False):
1212
1313
"""
1414

15-
DEFAULT = {"version": "unknown", "full": "unknown"}
15+
DEFAULT = {"version": "0+unknown", "full": "unknown"}
1616

1717

1818
def versions_from_file(filename):

src/git/from_keywords.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose=False):
5858
print("picking %s" % r)
5959
return {"version": r,
6060
"full": keywords["full"].strip()}
61-
# no suitable tags, so we use the full revision id
61+
# no suitable tags, so version is "0+unknown", but full hex is still there
6262
if verbose:
63-
print("no suitable tags, using full revision id")
64-
return {"version": keywords["full"].strip(),
63+
print("no suitable tags, using unknown + full revision id")
64+
return {"version": "0+unknown",
6565
"full": keywords["full"].strip()}
6666

src/git/from_vcs.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,50 @@
1+
import re # --STRIP DURING BUILD
2+
3+
def git_parse_vcs_describe(git_describe, tag_prefix, verbose=False):
4+
# TAG-NUM-gHEX[-dirty] or HEX[-dirty] . TAG might have hyphens.
5+
6+
# dirty
7+
dirty = git_describe.endswith("-dirty")
8+
if dirty:
9+
git_describe = git_describe[:git_describe.rindex("-dirty")]
10+
dirty_suffix = ".dirty" if dirty else ""
11+
12+
# now we have TAG-NUM-gHEX or HEX
13+
14+
if "-" not in git_describe: # just HEX
15+
return "0+untagged.g"+git_describe+dirty_suffix, dirty
16+
17+
# just TAG-NUM-gHEX
18+
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
19+
if not mo:
20+
# unparseable. Maybe git-describe is misbehaving?
21+
return "0+unparseable"+dirty_suffix, dirty
22+
23+
# tag
24+
full_tag = mo.group(1)
25+
if not full_tag.startswith(tag_prefix):
26+
if verbose:
27+
fmt = "tag '%s' doesn't start with prefix '%s'"
28+
print(fmt % (full_tag, tag_prefix))
29+
return None, dirty
30+
tag = full_tag[len(tag_prefix):]
31+
32+
# distance: number of commits since tag
33+
distance = int(mo.group(2))
34+
35+
# commit: short hex revision ID
36+
commit = mo.group(3)
37+
38+
# now build up version string, with post-release "local version
39+
# identifier". Our goal: TAG[+NUM.gHEX[.dirty]] . Note that if you get a
40+
# tagged build and then dirty it, you'll get TAG+0.gHEX.dirty . So you
41+
# can always test version.endswith(".dirty").
42+
version = tag
43+
if distance or dirty:
44+
version += "+%d.g%s" % (distance, commit) + dirty_suffix
45+
46+
return version, dirty
47+
148

249
def git_versions_from_vcs(tag_prefix, root, verbose=False):
350
# this runs 'git' from the root of the source tree. This only gets called
@@ -8,26 +55,28 @@ def git_versions_from_vcs(tag_prefix, root, verbose=False):
855
if not os.path.exists(os.path.join(root, ".git")):
956
if verbose:
1057
print("no .git in %s" % root)
11-
return {}
58+
return {} # get_versions() will try next method
1259

1360
GITS = ["git"]
1461
if sys.platform == "win32":
1562
GITS = ["git.cmd", "git.exe"]
16-
stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"],
63+
# if there is a tag, this yields TAG-NUM-gHEX[-dirty]
64+
# if there are no tags, this yields HEX[-dirty] (no NUM)
65+
stdout = run_command(GITS, ["describe", "--tags", "--dirty",
66+
"--always", "--long"],
1767
cwd=root)
68+
# --long was added in git-1.5.5
1869
if stdout is None:
19-
return {}
20-
if not stdout.startswith(tag_prefix):
21-
if verbose:
22-
fmt = "tag '%s' doesn't start with prefix '%s'"
23-
print(fmt % (stdout, tag_prefix))
24-
return {}
25-
tag = stdout[len(tag_prefix):]
70+
return {} # try next method
71+
version, dirty = git_parse_vcs_describe(stdout, tag_prefix, verbose)
72+
73+
# build "full", which is FULLHEX[.dirty]
2674
stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
2775
if stdout is None:
2876
return {}
2977
full = stdout.strip()
30-
if tag.endswith("-dirty"):
31-
full += "-dirty"
32-
return {"version": tag, "full": full}
78+
if dirty:
79+
full += ".dirty"
80+
81+
return {"version": version, "full": full}
3382

src/git/long_get_versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
2+
def get_versions(default={"version": "0+unknown", "full": ""}, verbose=False):
33
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
44
# __file__, we can work backwards from there to the root. Some
55
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which

test/test_git.py

Lines changed: 61 additions & 15 deletions
< 10000 td data-grid-cell-id="diff-84c4b72d5c62026240fad69041a453efc37f7336a89035b69672cce0e8beaca1-266-306-1" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionNum-bgColor, var(--diffBlob-addition-bgColor-num));text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative left-side">306
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,38 @@
55
import tarfile
66
import unittest
77
import tempfile
8+
from pkg_resources import parse_version, SetuptoolsLegacyVersion
89

910
sys.path.insert(0, "src")
11+
from git.from_vcs import git_parse_vcs_describe
1012
from git.from_keywords import git_versions_from_keywords
1113
from subprocess_helper import run_command
1214

1315
GITS = ["git"]
1416
if sys.platform == "win32":
1517
GITS = ["git.cmd", "git.exe"]
1618

19+
class ParseGitDescribe(unittest.TestCase):
20+
def test_parse(self):
21+
def pv(git_describe):
22+
return git_parse_vcs_describe(git_describe, "v")
23+
self.assertEqual(pv("1f"), ("0+untagged.g1f", False))
24+
self.assertEqual(pv("1f-dirty"), ("0+untagged.g1f.dirty", True))
25+
self.assertEqual(pv("v1.0-0-g1f"), ("1.0", False))
26+
self.assertEqual(pv("v1.0-0-g1f-dirty"), ("1.0+0.g1f.dirty", True))
27+
self.assertEqual(pv("v1.0-1-g1f"), ("1.0+1.g1f", False))
28+
self.assertEqual(pv("v1.0-1-g1f-dirty"), ("1.0+1.g1f.dirty", True))
29+
30+
def p(git_describe):
31+
return git_parse_vcs_describe(git_describe, "")
32+
self.assertEqual(p("1f"), ("0+untagged.g1f", False))
33+
self.assertEqual(p("1f-dirty"), ("0+untagged.g1f.dirty", True))
34+
self.assertEqual(p("1.0-0-g1f"), ("1.0", False))
35+
self.assertEqual(p("1.0-0-g1f-dirty"), ("1.0+0.g1f.dirty", True))
36+
self.assertEqual(p("1.0-1-g1f"), ("1.0+1.g1f", False))
37+
self.assertEqual(p("1.0-1-g1f-dirty"), ("1.0+1.g1f.dirty", True))
38+
39+
1740
class Keywords(unittest.TestCase):
1841
def parse(self, refnames, full, prefix=""):
1942
return git_versions_from_keywords({"refnames": refnames, "full": full},
@@ -40,12 +63,12 @@ def test_unexpanded(self):
4063

4164
def test_no_tags(self):
4265
v = self.parse("(HEAD, master)", "full")
43-
self.assertEqual(v["version"], "full")
66+
self.assertEqual(v["version"], "0+unknown")
4467
self.assertEqual(v["full"], "full")
4568

4669
def test_no_prefix(self):
4770
v = self.parse("(HEAD, master, 1.23)", "full", "missingprefix-")
48-
self.assertEqual(v["version"], "full")
71+
self.assertEqual(v["version"], "0+unkn 17AE own")
4972
self.assertEqual(v["full"], "full")
5073

5174
VERBOSE = False
@@ -72,6 +95,7 @@ def subpath(self, path):
7295
# SA: sitting on the 1.0 tag
7396
# SB: dirtying the tree after 1.0
7497
# SC: making a new commit after 1.0, clean tree
98+
# SD: dirtying the tree after the post-1.0 commit
7599
#
76100
# Then we're interested in 5 kinds of trees:
77101
# TA: source tree (with .git)
@@ -134,11 +158,12 @@ def run_test(self, demoapp_dir, script_only):
134158
self.git("add", "--all")
135159
self.git("commit", "-m", "comment")
136160

161+
full = self.git("rev-parse", "HEAD")
137162
v = self.python("setup.py", "--version")
138-
self.assertEqual(v, "unknown")
163+
self.assertEqual(v, "0+untagged.g%s" % full[:7])
139164
v = self.python(os.path.join(self.subpath("demoapp"), "setup.py"),
140165
"--version", workdir=self.testdir)
141-
self.assertEqual(v, "unknown")
166+
self.assertEqual(v, "0+untagged.g%s" % full[:7])
142167

143168
out = self.python("setup.py", "versioneer").splitlines()
144169
self.assertEqual(out[0], "running versioneer")
@@ -195,15 +220,24 @@ def run_test(self, demoapp_dir, script_only):
195220
f = open(self.subpath("demoapp/setup.py"),"a")
196221
f.write("# dirty\n")
197222
f.close()
198-
self.do_checks("1.0-dirty", full+"-dirty", dirty=True, state="SB")
223+
short = "1.0+0.g%s.dirty" % full[:7]
224+
self.do_checks(short, full+".dirty", dirty=True, state="SB")
199225

200226
# SC: now we make one commit past the tag
201227
self.git("add", "setup.py")
202228
self.git("commit", "-m", "dirty")
203229
full = self.git("rev-parse", "HEAD")
204-
short = "1.0-1-g%s" % full[:7]
230+
short = "1.0+1.g%s" % full[:7]
205231
self.do_checks(short, full, dirty=False, state="SC")
206232

233+
# SD: dirty the post-tag tree
234+
f = open(self.subpath("demoapp/setup.py"),"a")
235+
f.write("# more dirty\n")
236+
f.close()
237+
full = self.git("rev-parse", "HEAD")
238+
short = "1.0+1.g%s.dirty" % full[:7]
239+
self.do_checks(short, full+".dirty", dirty=True, state="SD")
240+
207241

208242
def do_checks(self, exp_short, exp_long, dirty, state):
209243
if os.path.exists(self.subpath("out")):
@@ -218,7 +252,7 @@ def do_checks(self, exp_short, exp_long, dirty, state):
218252
target = self.subpath("out/demoapp-TB")
219253
shutil.copytree(self.subpath("demoapp"), target)
220254
shutil.rmtree(os.path.join(target, ".git"))
221-
self.check_version(target, "unknown", "unknown", False, state, tree="TB")
255+
self.check_version(target, "0+unknown", "unknown", False, state, tree="TB")
222256

223257
# TC: source tree in versionprefix-named parentdir
224258
target = self.subpath("out/demo-1.1")
@@ -239,8 +273,8 @@ def do_checks(self, exp_short, exp_long, dirty, state):
239273
# expanded keywords only tell us about tags and full revisionids,
240274
# not how many patches we are beyond a tag. So we can't expect
241275
# the short version to be like 1.0-1-gHEXID. The code falls back
242-
# to short=long
243-
exp_short_TD = exp_long
276+
# to short="unknown"
277+
exp_short_TD = "0+unknown"
244278
self.check_version(target, exp_short_TD, exp_long, False, state, tree="TD")
245279

246280
# TE: unpacked setup.py sdist tarball
@@ -260,10 +294,18 @@ def do_checks(self, exp_short, exp_long, dirty, state):
260294
self.assertTrue(os.path.isdir(target))
261295
self.check_version(target, exp_short, exp_long, False, state, tree="TE")
262296

263-
def compare(self, got, expected, state, tree, runtime):
297+
def compare(self, got, expected, state, tree, runtime, pep440):
264298
where = "/".join([state, tree, runtime])
265299
self.assertEqual(got, expected, "%s: got '%s' != expected '%s'"
266300
% (where, got, expected))
301+
if pep440:
302+
pv = parse_version(got)
303+
self.assertFalse(isinstance(pv, SetuptoolsLegacyVersion),
304+
"%s: '%s' was not pep440-compatible"
305+
% (where, got))
+
self.assertEqual(str(pv), got,
307+
"%s: '%s' pep440-normalized to '%s'"
308+
% (where, got, str(pv)))
267309
if VERBOSE: print(" good %s" % where)
268310

269311
def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
@@ -275,11 +317,12 @@ def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
275317
print(self.python("setup.py", "version", workdir=workdir))
276318
# setup.py --version gives us get_version() with verbose=False.
277319
v = self.python("setup.py", "--version", workdir=workdir)
278-
self.compare(v, exp_short, state, tree, "RA1")
320+
self.compare(v, exp_short, state, tree, "RA1", pep440=True)
321+
279322
# and test again from outside the tree
280323
v = self.python(os.path.join(workdir, "setup.py"), "--version",
281324
workdir=self.testdir)
282-
self.compare(v, exp_short, state, tree, "RA2")
325+
self.compare(v, exp_short, state, tree, "RA2", pep440=True)
283326

284327
if dirty:
285328
return # cannot detect dirty files in a build # XXX really?
@@ -292,9 +335,12 @@ def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
292335
build_lib = os.path.join(workdir, "build", "lib")
293336
out = self.python("rundemo", "--version", workdir=build_lib)
294337
data = dict([line.split(":",1) for line in out.splitlines()])
295-
self.compare(data["__version__"], exp_short, state, tree, "RB")
296-
self.compare(data["shortversion"], exp_short, state, tree, "RB")
297-
self.compare(data["longversion"], exp_long, state, tree, "RB")
338+
self.compare(data["__version__"], exp_short, state, tree, "RB",
339+
pep440=True)
340+
self.compare(data["shortversion"], exp_short, state, tree, "RB",
341+
pep440=True)
342+
self.compare(data["longversion"], exp_long, state, tree, "RB",
343+
pep440=False)
298344

299345

300346
if __name__ == '__main__':

0 commit comments

Comments
 (0)
0