_version.py 18 KB

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