Coverage for gwcelery/_version.py: 42%
279 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-11-14 05:52 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-11-14 05:52 +0000
2# This file helps to compute a version number in source trees obtained from
3# git-archive tarball (such as those provided by githubs download-from-tag
4# feature). Distribution tarballs (built by setup.py sdist) and build
5# directories (produced by setup.py build) will contain a much shorter file
6# that just contains the computed version number.
8# This file is released into the public domain. Generated by
9# versioneer-0.18 (https://github.com/warner/python-versioneer)
11"""Git implementation of _version.py."""
13import errno
14import os
15import re
16import subprocess
17import sys
20def get_keywords():
21 """Get the keywords needed to look up the version information."""
22 # these strings will be replaced by git during git-archive.
23 # setup.py/versioneer.py will grep for the variable names, so they must
24 # each be defined on a line of their own. _version.py will just call
25 # get_keywords().
26 git_refnames = "$Format:%d$"
27 git_full = "$Format:%H$"
28 git_date = "$Format:%ci$"
29 keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
30 return keywords
33class VersioneerConfig:
34 """Container for Versioneer configuration parameters."""
37def get_config():
38 """Create, populate and return the VersioneerConfig() object."""
39 # these strings are filled in when 'setup.py versioneer' creates
40 # _version.py
41 cfg = VersioneerConfig()
42 cfg.VCS = "git"
43 cfg.style = ""
44 cfg.tag_prefix = "v"
45 cfg.parentdir_prefix = "gwcelery-"
46 cfg.versionfile_source = "gwcelery/_version.py"
47 cfg.verbose = False
48 return cfg
51class NotThisMethod(Exception):
52 """Exception raised if a method is not valid for the current scenario."""
55LONG_VERSION_PY = {}
56HANDLERS = {}
59def register_vcs_handler(vcs, method): # decorator
60 """Decorator to mark a method as the handler for a particular VCS."""
61 def decorate(f):
62 """Store f in HANDLERS[vcs][method]."""
63 if vcs not in HANDLERS:
64 HANDLERS[vcs] = {}
65 HANDLERS[vcs][method] = f
66 return f
67 return decorate
70def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
71 env=None):
72 """Call the given command(s)."""
73 assert isinstance(commands, list)
74 p = None
75 for c in commands:
76 try:
77 dispcmd = str([c] + args)
78 # remember shell=False, so use git.cmd on windows, not just git
79 p = subprocess.Popen([c] + args, cwd=cwd, env=env,
80 stdout=subprocess.PIPE,
81 stderr=(subprocess.PIPE if hide_stderr
82 else None))
83 break
84 except EnvironmentError:
85 e = sys.exc_info()[1]
86 if e.errno == errno.ENOENT:
87 continue
88 if verbose:
89 print("unable to run %s" % dispcmd)
90 print(e)
91 return None, None
92 else:
93 if verbose:
94 print("unable to find command, tried %s" % (commands,))
95 return None, None
96 stdout = p.communicate()[0].strip()
97 if sys.version_info[0] >= 3:
98 stdout = stdout.decode()
99 if p.returncode != 0:
100 if verbose:
101 print("unable to run %s (error)" % dispcmd)
102 print("stdout was %s" % stdout)
103 return None, p.returncode
104 return stdout, p.returncode
107def versions_from_parentdir(parentdir_prefix, root, verbose):
108 """Try to determine the version from the parent directory name.
110 Source tarballs conventionally unpack into a directory that includes both
111 the project name and a version string. We will also support searching up
112 two directory levels for an appropriately named parent directory
113 """
114 rootdirs = []
116 for i in range(3):
117 dirname = os.path.basename(root)
118 if dirname.startswith(parentdir_prefix):
119 return {"version": dirname[len(parentdir_prefix):],
120 "full-revisionid": None,
121 "dirty": False, "error": None, "date": None}
122 else:
123 rootdirs.append(root)
124 root = os.path.dirname(root) # up a level
126 if verbose:
127 print("Tried directories %s but none started with prefix %s" %
128 (str(rootdirs), parentdir_prefix))
129 raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
132@register_vcs_handler("git", "get_keywords")
133def git_get_keywords(versionfile_abs):
134 """Extract version information from the given file."""
135 # the code embedded in _version.py can just fetch the value of these
136 # keywords. When used from setup.py, we don't want to import _version.py,
137 # so we do it with a regexp instead. This function is not used from
138 # _version.py.
139 keywords = {}
140 try:
141 f = open(versionfile_abs, "r")
142 for line in f.readlines():
143 if line.strip().startswith("git_refnames ="):
144 mo = re.search(r'=\s*"(.*)"', line)
145 if mo:
146 keywords["refnames"] = mo.group(1)
147 if line.strip().startswith("git_full ="):
148 mo = re.search(r'=\s*"(.*)"', line)
149 if mo:
150 keywords["full"] = mo.group(1)
151 if line.strip().startswith("git_date ="):
152 mo = re.search(r'=\s*"(.*)"', line)
153 if mo:
154 keywords["date"] = mo.group(1)
155 f.close()
156 except EnvironmentError:
157 pass
158 return keywords
161@register_vcs_handler("git", "keywords")
162def git_versions_from_keywords(keywords, tag_prefix, verbose):
163 """Get version information from git keywords."""
164 if not keywords:
165 raise NotThisMethod("no keywords at all, weird")
166 date = keywords.get("date")
167 if date is not None:
168 # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
169 # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
170 # -like" string, which we must then edit to make compliant), because
171 # it's been around since git-1.5.3, and it's too difficult to
172 # discover which version we're using, or to work around using an
173 # older one.
174 date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
175 refnames = keywords["refnames"].strip()
176 if refnames.startswith("$Format"):
177 if verbose:
178 print("keywords are unexpanded, not using")
179 raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
180 refs = set([r.strip() for r in refnames.strip("()").split(",")])
181 # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
182 # just "foo-1.0". If we see a "tag: " prefix, prefer those.
183 TAG = "tag: "
184 tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
185 if not tags:
186 # Either we're using git < 1.8.3, or there really are no tags. We use
187 # a heuristic: assume all version tags have a digit. The old git %d
188 # expansion behaves like git log --decorate=short and strips out the
189 # refs/heads/ and refs/tags/ prefixes that would let us distinguish
190 # between branches and tags. By ignoring refnames without digits, we
191 # filter out many common branch names like "release" and
192 # "stabilization", as well as "HEAD" and "main".
193 tags = set([r for r in refs if re.search(r'\d', r)])
194 if verbose:
195 print("discarding '%s', no digits" % ",".join(refs - tags))
196 if verbose:
197 print("likely tags: %s" % ",".join(sorted(tags)))
198 for ref in sorted(tags):
199 # sorting will prefer e.g. "2.0" over "2.0rc1"
200 if ref.startswith(tag_prefix):
201 r = ref[len(tag_prefix):]
202 if verbose:
203 print("picking %s" % r)
204 return {"version": r,
205 "full-revisionid": keywords["full"].strip(),
206 "dirty": False, "error": None,
207 "date": date}
208 # no suitable tags, so version is "0+unknown", but full hex is still there
209 if verbose:
210 print("no suitable tags, using unknown + full revision id")
211 return {"version": "0+unknown",
212 "full-revisionid": keywords["full"].strip(),
213 "dirty": False, "error": "no suitable tags", "date": None}
216@register_vcs_handler("git", "pieces_from_vcs")
217def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
218 """Get version from 'git describe' in the root of the source tree.
220 This only gets called if the git-archive 'subst' keywords were *not*
221 expanded, and _version.py hasn't already been rewritten with a short
222 version string, meaning we're inside a checked out source tree.
223 """
224 GITS = ["git"]
225 if sys.platform == "win32":
226 GITS = ["git.cmd", "git.exe"]
228 out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
229 hide_stderr=True)
230 if rc != 0:
231 if verbose:
232 print("Directory %s not under git control" % root)
233 raise NotThisMethod("'git rev-parse --git-dir' returned error")
235 # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
236 # if there isn't one, this yields HEX[-dirty] (no NUM)
237 describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
238 "--always", "--long",
239 "--match", "%s*" % tag_prefix],
240 cwd=root)
241 # --long was added in git-1.5.5
242 if describe_out is None:
243 raise NotThisMethod("'git describe' failed")
244 describe_out = describe_out.strip()
245 full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
246 if full_out is None:
247 raise NotThisMethod("'git rev-parse' failed")
248 full_out = full_out.strip()
250 pieces = {}
251 pieces["long"] = full_out
252 pieces["short"] = full_out[:7] # maybe improved later
253 pieces["error"] = None
255 # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
256 # TAG might have hyphens.
257 git_describe = describe_out
259 # look for -dirty suffix
260 dirty = git_describe.endswith("-dirty")
261 pieces["dirty"] = dirty
262 if dirty:
263 git_describe = git_describe[:git_describe.rindex("-dirty")]
265 # now we have TAG-NUM-gHEX or HEX
267 if "-" in git_describe:
268 # TAG-NUM-gHEX
269 mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
270 if not mo:
271 # unparseable. Maybe git-describe is misbehaving?
272 pieces["error"] = ("unable to parse git-describe output: '%s'"
273 % describe_out)
274 return pieces
276 # tag
277 full_tag = mo.group(1)
278 if not full_tag.startswith(tag_prefix):
279 if verbose:
280 fmt = "tag '%s' doesn't start with prefix '%s'"
281 print(fmt % (full_tag, tag_prefix))
282 pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
283 % (full_tag, tag_prefix))
284 return pieces
285 pieces["closest-tag"] = full_tag[len(tag_prefix):]
287 # distance: number of commits since tag
288 pieces["distance"] = int(mo.group(2))
290 # commit: short hex revision ID
291 pieces["short"] = mo.group(3)
293 else:
294 # HEX: no tags
295 pieces["closest-tag"] = None
296 count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
297 cwd=root)
298 pieces["distance"] = int(count_out) # total number of commits
300 # commit date: see ISO-8601 comment in git_versions_from_keywords()
301 date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
302 cwd=root)[0].strip()
303 pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
305 return pieces
308def plus_or_dot(pieces):
309 """Return a + if we don't already have one, else return a ."""
310 if "+" in pieces.get("closest-tag", ""):
311 return "."
312 return "+"
315def render_pep440(pieces):
316 """Build up version string, with post-release "local version identifier".
318 Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
319 get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
321 Exceptions:
322 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
323 """
324 if pieces["closest-tag"]:
325 rendered = pieces["closest-tag"]
326 if pieces["distance"] or pieces["dirty"]:
327 rendered += plus_or_dot(pieces)
328 rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
329 if pieces["dirty"]:
330 rendered += ".dirty"
331 else:
332 # exception #1
333 rendered = "0+untagged.%d.g%s" % (pieces["distance"],
334 pieces["short"])
335 if pieces["dirty"]:
336 rendered += ".dirty"
337 return rendered
340def render_pep440_pre(pieces):
341 """TAG[.post.devDISTANCE] -- No -dirty.
343 Exceptions:
344 1: no tags. 0.post.devDISTANCE
345 """
346 if pieces["closest-tag"]:
347 rendered = pieces["closest-tag"]
348 if pieces["distance"]:
349 rendered += ".post.dev%d" % pieces["distance"]
350 else:
351 # exception #1
352 rendered = "0.post.dev%d" % pieces["distance"]
353 return rendered
356def render_pep440_post(pieces):
357 """TAG[.postDISTANCE[.dev0]+gHEX] .
359 The ".dev0" means dirty. Note that .dev0 sorts backwards
360 (a dirty tree will appear "older" than the corresponding clean one),
361 but you shouldn't be releasing software with -dirty anyways.
363 Exceptions:
364 1: no tags. 0.postDISTANCE[.dev0]
365 """
366 if pieces["closest-tag"]:
367 rendered = pieces["closest-tag"]
368 if pieces["distance"] or pieces["dirty"]:
369 rendered += ".post%d" % pieces["distance"]
370 if pieces["dirty"]:
371 rendered += ".dev0"
372 rendered += plus_or_dot(pieces)
373 rendered += "g%s" % pieces["short"]
374 else:
375 # exception #1
376 rendered = "0.post%d" % pieces["distance"]
377 if pieces["dirty"]:
378 rendered += ".dev0"
379 rendered += "+g%s" % pieces["short"]
380 return rendered
383def render_pep440_old(pieces):
384 """TAG[.postDISTANCE[.dev0]] .
386 The ".dev0" means dirty.
388 Eexceptions:
389 1: no tags. 0.postDISTANCE[.dev0]
390 """
391 if pieces["closest-tag"]:
392 rendered = pieces["closest-tag"]
393 if pieces["distance"] or pieces["dirty"]:
394 rendered += ".post%d" % pieces["distance"]
395 if pieces["dirty"]:
396 rendered += ".dev0"
397 else:
398 # exception #1
399 rendered = "0.post%d" % pieces["distance"]
400 if pieces["dirty"]:
401 rendered += ".dev0"
402 return rendered
405def render_git_describe(pieces):
406 """TAG[-DISTANCE-gHEX][-dirty].
408 Like 'git describe --tags --dirty --always'.
410 Exceptions:
411 1: no tags. HEX[-dirty] (note: no 'g' prefix)
412 """
413 if pieces["closest-tag"]:
414 rendered = pieces["closest-tag"]
415 if pieces["distance"]:
416 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
417 else:
418 # exception #1
419 rendered = pieces["short"]
420 if pieces["dirty"]:
421 rendered += "-dirty"
422 return rendered
425def render_git_describe_long(pieces):
426 """TAG-DISTANCE-gHEX[-dirty].
428 Like 'git describe --tags --dirty --always -long'.
429 The distance/hash is unconditional.
431 Exceptions:
432 1: no tags. HEX[-dirty] (note: no 'g' prefix)
433 """
434 if pieces["closest-tag"]:
435 rendered = pieces["closest-tag"]
436 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
437 else:
438 # exception #1
439 rendered = pieces["short"]
440 if pieces["dirty"]:
441 rendered += "-dirty"
442 return rendered
445def render(pieces, style):
446 """Render the given version pieces into the requested style."""
447 if pieces["error"]:
448 return {"version": "unknown",
449 "full-revisionid": pieces.get("long"),
450 "dirty": None,
451 "error": pieces["error"],
452 "date": None}
454 if not style or style == "default":
455 style = "pep440" # the default
457 if style == "pep440":
458 rendered = render_pep440(pieces)
459 elif style == "pep440-pre":
460 rendered = render_pep440_pre(pieces)
461 elif style == "pep440-post":
462 rendered = render_pep440_post(pieces)
463 elif style == "pep440-old":
464 rendered = render_pep440_old(pieces)
465 elif style == "git-describe":
466 rendered = render_git_describe(pieces)
467 elif style == "git-describe-long":
468 rendered = render_git_describe_long(pieces)
469 else:
470 raise ValueError("unknown style '%s'" % style)
472 return {"version": rendered, "full-revisionid": pieces["long"],
473 "dirty": pieces["dirty"], "error": None,
474 "date": pieces.get("date")}
477def get_versions():
478 """Get version information or return default if unable to do so."""
479 # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
480 # __file__, we can work backwards from there to the root. Some
481 # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
482 # case we can only use expanded keywords.
484 cfg = get_config()
485 verbose = cfg.verbose
487 try:
488 return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
489 verbose)
490 except NotThisMethod:
491 pass
493 try:
494 root = os.path.realpath(__file__)
495 # versionfile_source is the relative path from the top of the source
496 # tree (where the .git directory might live) to this file. Invert
497 # this to find the root from __file__.
498 for i in cfg.versionfile_source.split('/'):
499 root = os.path.dirname(root)
500 except NameError:
501 return {"version": "0+unknown", "full-revisionid": None,
502 "dirty": None,
503 "error": "unable to find root of source tree",
504 "date": None}
506 try:
507 pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
508 return render(pieces, cfg.style)
509 except NotThisMethod:
510 pass
512 try:
513 if cfg.parentdir_prefix:
514 return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
515 except NotThisMethod:
516 pass
518 return {"version": "0+unknown", "full-revisionid": None,
519 "dirty": None,
520 "error": "unable to compute version", "date": None}