Coverage for gwcelery/_version.py: 45%

279 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-25 18:01 +0000

1 

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. 

7 

8# This file is released into the public domain. Generated by 

9# versioneer-0.18 (https://github.com/warner/python-versioneer) 

10 

11"""Git implementation of _version.py.""" 

12 

13import errno 

14import os 

15import re 

16import subprocess 

17import sys 

18 

19 

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 

31 

32 

33class VersioneerConfig: 

34 """Container for Versioneer configuration parameters.""" 

35 

36 

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 

49 

50 

51class NotThisMethod(Exception): 

52 """Exception raised if a method is not valid for the current scenario.""" 

53 

54 

55LONG_VERSION_PY = {} 

56HANDLERS = {} 

57 

58 

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 

68 

69 

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 

105 

106 

107def versions_from_parentdir(parentdir_prefix, root, verbose): 

108 """Try to determine the version from the parent directory name. 

109 

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 = [] 

115 

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 

125 

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") 

130 

131 

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 

159 

160 

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} 

214 

215 

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. 

219 

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"] 

227 

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") 

234 

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() 

249 

250 pieces = {} 

251 pieces["long"] = full_out 

252 pieces["short"] = full_out[:7] # maybe improved later 

253 pieces["error"] = None 

254 

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 

258 

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")] 

264 

265 # now we have TAG-NUM-gHEX or HEX 

266 

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 

275 

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):] 

286 

287 # distance: number of commits since tag 

288 pieces["distance"] = int(mo.group(2)) 

289 

290 # commit: short hex revision ID 

291 pieces["short"] = mo.group(3) 

292 

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 

299 

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) 

304 

305 return pieces 

306 

307 

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 "+" 

313 

314 

315def render_pep440(pieces): 

316 """Build up version string, with post-release "local version identifier". 

317 

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 

320 

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 

338 

339 

340def render_pep440_pre(pieces): 

341 """TAG[.post.devDISTANCE] -- No -dirty. 

342 

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 

354 

355 

356def render_pep440_post(pieces): 

357 """TAG[.postDISTANCE[.dev0]+gHEX] . 

358 

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. 

362 

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 

381 

382 

383def render_pep440_old(pieces): 

384 """TAG[.postDISTANCE[.dev0]] . 

385 

386 The ".dev0" means dirty. 

387 

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 

403 

404 

405def render_git_describe(pieces): 

406 """TAG[-DISTANCE-gHEX][-dirty]. 

407 

408 Like 'git describe --tags --dirty --always'. 

409 

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 

423 

424 

425def render_git_describe_long(pieces): 

426 """TAG-DISTANCE-gHEX[-dirty]. 

427 

428 Like 'git describe --tags --dirty --always -long'. 

429 The distance/hash is unconditional. 

430 

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 

443 

444 

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} 

453 

454 if not style or style == "default": 

455 style = "pep440" # the default 

456 

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) 

471 

472 return {"version": rendered, "full-revisionid": pieces["long"], 

473 "dirty": pieces["dirty"], "error": None, 

474 "date": pieces.get("date")} 

475 

476 

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. 

483 

484 cfg = get_config() 

485 verbose = cfg.verbose 

486 

487 try: 

488 return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 

489 verbose) 

490 except NotThisMethod: 

491 pass 

492 

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} 

505 

506 try: 

507 pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 

508 return render(pieces, cfg.style) 

509 except NotThisMethod: 

510 pass 

511 

512 try: 

513 if cfg.parentdir_prefix: 

514 return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 

515 except NotThisMethod: 

516 pass 

517 

518 return {"version": "0+unknown", "full-revisionid": None, 

519 "dirty": None, 

520 "error": "unable to compute version", "date": None}