8000 PEP440 compliant versions by warner · Pull Request #67 · python-versioneer/python-versioneer · GitHub
[go: up one dir, main page]

Skip to content

PEP440 compliant versions #67

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,19 @@ import the top-level `versioneer.py` and run `get_versions()`.
Both functions return a dictionary with different keys for different flavors
of the version string:

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

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

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

In the future, this will also include a
[PEP-0440](http://legacy.python.org/dev/peps/pep-0440/) -compatible flavor
(e.g. `1.2.post0.dev123`). This loses a lot of information (and has no room
for a hash-based revision id), but is safe to use in a `setup.py`
"`version=`" argument. It also enables tools like *pip* to compare version
strings and evaluate compatibility constraint declarations.

The `setup.py versioneer` command adds the following text to your
`__init__.py` to place a basic version in `YOURPROJECT.__version__`:

Expand Down
2 changes: 1 addition & 1 deletion src/from_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def get_versions(default={}, verbose=False):

"""

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


def versions_from_file(filename):
Expand Down
6 changes: 3 additions & 3 deletions src/git/from_keywords.py
10000
Original file line number Diff line numberDiff line change
Expand Up @@ -58,9 +58,9 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose=False):
print("picking %s" % r)
return {"version": r,
"full": keywords["full"].strip()}
# no suitable tags, so we use the full revision id
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using full revision id")
return {"version": keywords["full"].strip(),
print("no suitable tags, using unknown + full revision id")
return {"version": "0+unknown",
"full": keywords["full"].strip()}

73 changes: 61 additions & 12 deletions src/git/from_vcs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
import re # --STRIP DURING BUILD

def git_parse_vcs_describe(git_describe, tag_prefix, verbose=False):
# TAG-NUM-gHEX[-dirty] or HEX[-dirty] . TAG might have hyphens.

# dirty
dirty = git_describe.endswith("-dirty")
if dirty:
git_describe = git_describe[:git_describe.rindex("-dirty")]
dirty_suffix = ".dirty" if dirty else ""

# now we have TAG-NUM-gHEX or HEX

if "-" not in git_describe: # just HEX
return "0+untagged.g"+git_describe+dirty_suffix, dirty

# just TAG-NUM-gHEX
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
return "0+unparseable"+dirty_suffix, dirty

# tag
full_tag = mo.group(1)
if not full_tag.startswith(tag_prefix):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
return None, dirty
tag = full_tag[len(tag_prefix):]

# distance: number of commits since tag
distance = int(mo.group(2))

# commit: short hex revision ID
commit = mo.group(3)

# now build up version string, with post-release "local version
# identifier". Our goal: TAG[+NUM.gHEX[.dirty]] . Note that if you get a
# tagged build and then dirty it, you'll get TAG+0.gHEX.dirty . So you
# can always test version.endswith(".dirty").
version = tag
if distance or dirty:
version += "+%d.g%s" % (distance, commit) + dirty_suffix

return version, dirty


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

GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
stdout = run_command(GITS, ["describe", "--tags", "--dirty", "--always"],
# if there is a tag, this yields TAG-NUM-gHEX[-dirty]
# if there are no tags, this yields HEX[-dirty] (no NUM)
stdout = run_command(GITS, ["describe", "--tags", "--dirty",
"--always", "--long"],
cwd=root)
# --long was added in git-1.5.5
if stdout is None:
return {}
if not stdout.startswith(tag_prefix):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (stdout, tag_prefix))
return {}
tag = stdout[len(tag_prefix):]
return {} # try next method
version, dirty = git_parse_vcs_describe(stdout, tag_prefix, verbose)

# build "full", which is FULLHEX[.dirty]
stdout = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
if stdout is None:
return {}
full = stdout.strip()
if tag.endswith("-dirty"):
full += "-dirty"
return {"version": tag, "full": full}
if dirty:
full += ".dirty"

return {"version": version, "full": full}

2 changes: 1 addition & 1 deletion src/git/long_get_versions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

def get_versions(default={"version": "unknown", "full": ""}, verbose=False):
def get_versions(default={"version": "0+unknown", "full": ""}, verbose=False):
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
# __file__, we can work backwards from there to the root. Some
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
Expand Down
76 changes: 61 additions & 15 deletions test/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,38 @@
import tarfile
import unittest
import tempfile
from pkg_resources import parse_version, SetuptoolsLegacyVersion

sys.path.insert(0, "src")
from git.from_vcs import git_parse_vcs_describe
from git.from_keywords import git_versions_from_keywords
from subprocess_helper import run_command

GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]

class ParseGitDescribe(unittest.TestCase):
def test_parse(self):
def pv(git_describe):
return git_parse_vcs_describe(git_describe, "v")
self.assertEqual(pv("1f"), ("0+untagged.g1f", False))
self.assertEqual(pv("1f-dirty"), ("0+untagged.g1f.dirty", True))
self.assertEqual(pv("v1.0-0-g1f"), ("1.0", False))
self.assertEqual(pv("v1.0-0-g1f-dirty"), ("1.0+0.g1f.dirty", True))
self.assertEqual(pv("v1.0-1-g1f"), ("1.0+1.g1f", False))
self.assertEqual(pv("v1.0-1-g1f-dirty"), ("1.0+1.g1f.dirty", True))

def p(git_describe):
return git_parse_vcs_describe(git_describe, "")
self.assertEqual(p("1f"), ("0+untagged.g1f", False))
self.assertEqual(p("1f-dirty"), ("0+untagged.g1f.dirty", True))
self.assertEqual(p("1.0-0-g1f"), ("1.0", False))
self.assertEqual(p("1.0-0-g1f-dirty"), ("1.0+0.g1f.dirty", True))
self.assertEqual(p("1.0-1-g1f"), ("1.0+1.g1f", False))
self.assertEqual(p("1.0-1-g1f-dirty"), ("1.0+1.g1f.dirty", True))


class Keywords(unittest.TestCase):
def parse(self, refnames, full, prefix=""):
return git_versions_from_keywords({"refnames": refnames, "full": full},
Expand All @@ -40,12 +63,12 @@ def test_unexpanded(self):

def test_no_tags(self):
v = self.parse("(HEAD, master)", "full")
self.assertEqual(v["version"], "full")
self.assertEqual(v["version"], "0+unknown")
self.assertEqual(v["full"], "full")

def test_no_prefix(self):
v = self.parse("(HEAD, master, 1.23)", "full", "missingprefix-")
self.assertEqual(v["version"], "full")
self.assertEqual(v["version"], "0+unknown")
self.assertEqual(v["full"], "full")

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

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

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

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

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


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

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

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

def compare(self, got, expected, state, tree, runtime):
def compare(self, got, expected, state, tree, runtime, pep440):
where = "/".join([state, tree, runtime])
self.assertEqual(got, expected, "%s: got '%s' != expected '%s'"
% (where, got, expected))
if pep440:
pv = parse_version(got)
self.assertFalse(isinstance(pv, SetuptoolsLegacyVersion),
"%s: '%s' was not pep440-compatible"
% (where, got))
self.assertEqual(str(pv), got,
"%s: '%s' pep440-normalized to '%s'"
% (where, got, str(pv)))
if VERBOSE: print(" good %s" % where)

def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
Expand All @@ -275,11 +317,12 @@ def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
print(self.python("setup.py", "version", workdir=workdir))
# setup.py --version gives us get_version() with verbose=False.
v = self.python("setup.py", "--version", workdir=workdir)
self.compare(v, exp_short, state, tree, "RA1")
self.compare(v, exp_short, state, tree, "RA1", pep440=True)

# and test again from outside the tree
v = self.python(os.path.join(workdir, "setup.py"), "--version",
workdir=self.testdir)
self.compare(v, exp_short, state, tree, "RA2")
self.compare(v, exp_short, state, tree, "RA2", pep440=True)

if dirty:
return # cannot detect dirty files in a build # XXX really?
Expand All @@ -292,9 +335,12 @@ def check_version(self, workdir, exp_short, exp_long, dirty, state, tree):
build_lib = os.path.join(workdir, "build", "lib")
out = self.python("rundemo", "--version", workdir=build_lib)
data = dict([line.split(":",1) for line in out.splitlines()])
self.compare(data["__version__"], exp_short, state, tree, "RB")
self.compare(data["shortversion"], exp_short, state, tree, "RB")
self.compare(data["longversion"], exp_long, state, tree, "RB")
self.compare(data["__version__"], exp_short, state, tree, "RB",
pep440=True)
self.compare(data["shortversion"], exp_short, state, tree, "RB",
pep440=True)
self.compare(data["longversion"], exp_long, state, tree, "RB",
pep440=False)


if __name__ == '__main__':
Expand Down
0