Merge pull request 'Upgrade to 3.12' (#630) from python-3.12 into main
[inboxen.git] / inboxen / _version.py
blob64118dbc019246110bd066c32a8403f7825ab41b
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.
9 # Generated by versioneer-0.29
10 # https://github.com/python-versioneer/python-versioneer
12 """Git implementation of _version.py."""
14 import errno
15 import os
16 import re
17 import subprocess
18 import sys
19 from typing import Any, Callable, Dict, List, Optional, Tuple
20 import functools
23 def get_keywords() -> Dict[str, str]:
24 """Get the keywords needed to look up the version information."""
25 # these strings will be replaced by git during git-archive.
26 # setup.py/versioneer.py will grep for the variable names, so they must
27 # each be defined on a line of their own. _version.py will just call
28 # get_keywords().
29 git_refnames = "$Format:%d$"
30 git_full = "$Format:%H$"
31 git_date = "$Format:%ci$"
32 keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
33 return keywords
36 class VersioneerConfig:
37 """Container for Versioneer configuration parameters."""
39 VCS: str
40 style: str
41 tag_prefix: str
42 parentdir_prefix: str
43 versionfile_source: str
44 verbose: bool
47 def get_config() -> VersioneerConfig:
48 """Create, populate and return the VersioneerConfig() object."""
49 # these strings are filled in when 'setup.py versioneer' creates
50 # _version.py
51 cfg = VersioneerConfig()
52 cfg.VCS = "git"
53 cfg.style = "pep440"
54 cfg.tag_prefix = "deploy-"
55 cfg.parentdir_prefix = "inboxen-"
56 cfg.versionfile_source = "inboxen/_version.py"
57 cfg.verbose = False
58 return cfg
61 class NotThisMethod(Exception):
62 """Exception raised if a method is not valid for the current scenario."""
65 LONG_VERSION_PY: Dict[str, str] = {}
66 HANDLERS: Dict[str, Dict[str, Callable]] = {}
69 def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator
70 """Create decorator to mark a method as the handler of a VCS."""
71 def decorate(f: Callable) -> Callable:
72 """Store f in HANDLERS[vcs][method]."""
73 if vcs not in HANDLERS:
74 HANDLERS[vcs] = {}
75 HANDLERS[vcs][method] = f
76 return f
77 return decorate
80 def run_command(
81 commands: List[str],
82 args: List[str],
83 cwd: Optional[str] = None,
84 verbose: bool = False,
85 hide_stderr: bool = False,
86 env: Optional[Dict[str, str]] = None,
87 ) -> Tuple[Optional[str], Optional[int]]:
88 """Call the given command(s)."""
89 assert isinstance(commands, list)
90 process = None
92 popen_kwargs: Dict[str, Any] = {}
93 if sys.platform == "win32":
94 # This hides the console window if pythonw.exe is used
95 startupinfo = subprocess.STARTUPINFO()
96 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
97 popen_kwargs["startupinfo"] = startupinfo
99 for command in commands:
100 try:
101 dispcmd = str([command] + args)
102 # remember shell=False, so use git.cmd on windows, not just git
103 process = subprocess.Popen([command] + args, cwd=cwd, env=env,
104 stdout=subprocess.PIPE,
105 stderr=(subprocess.PIPE if hide_stderr
106 else None), **popen_kwargs)
107 break
108 except OSError as e:
109 if e.errno == errno.ENOENT:
110 continue
111 if verbose:
112 print("unable to run %s" % dispcmd)
113 print(e)
114 return None, None
115 else:
116 if verbose:
117 print("unable to find command, tried %s" % (commands,))
118 return None, None
119 stdout = process.communicate()[0].strip().decode()
120 if process.returncode != 0:
121 if verbose:
122 print("unable to run %s (error)" % dispcmd)
123 print("stdout was %s" % stdout)
124 return None, process.returncode
125 return stdout, process.returncode
128 def versions_from_parentdir(
129 parentdir_prefix: str,
130 root: str,
131 verbose: bool,
132 ) -> Dict[str, Any]:
133 """Try to determine the version from the parent directory name.
135 Source tarballs conventionally unpack into a directory that includes both
136 the project name and a version string. We will also support searching up
137 two directory levels for an appropriately named parent directory
139 rootdirs = []
141 for _ in range(3):
142 dirname = os.path.basename(root)
143 if dirname.startswith(parentdir_prefix):
144 return {"version": dirname[len(parentdir_prefix):],
145 "full-revisionid": None,
146 "dirty": False, "error": None, "date": None}
147 rootdirs.append(root)
148 root = os.path.dirname(root) # up a level
150 if verbose:
151 print("Tried directories %s but none started with prefix %s" %
152 (str(rootdirs), parentdir_prefix))
153 raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
156 @register_vcs_handler("git", "get_keywords")
157 def git_get_keywords(versionfile_abs: str) -> Dict[str, str]:
158 """Extract version information from the given file."""
159 # the code embedded in _version.py can just fetch the value of these
160 # keywords. When used from setup.py, we don't want to import _version.py,
161 # so we do it with a regexp instead. This function is not used from
162 # _version.py.
163 keywords: Dict[str, str] = {}
164 try:
165 with open(versionfile_abs, "r") as fobj:
166 for line in fobj:
167 if line.strip().startswith("git_refnames ="):
168 mo = re.search(r'=\s*"(.*)"', line)
169 if mo:
170 keywords["refnames"] = mo.group(1)
171 if line.strip().startswith("git_full ="):
172 mo = re.search(r'=\s*"(.*)"', line)
173 if mo:
174 keywords["full"] = mo.group(1)
175 if line.strip().startswith("git_date ="):
176 mo = re.search(r'=\s*"(.*)"', line)
177 if mo:
178 keywords["date"] = mo.group(1)
179 except OSError:
180 pass
181 return keywords
184 @register_vcs_handler("git", "keywords")
185 def git_versions_from_keywords(
186 keywords: Dict[str, str],
187 tag_prefix: str,
188 verbose: bool,
189 ) -> Dict[str, Any]:
190 """Get version information from git keywords."""
191 if "refnames" not in keywords:
192 raise NotThisMethod("Short version file found")
193 date = keywords.get("date")
194 if date is not None:
195 # Use only the last line. Previous lines may contain GPG signature
196 # information.
197 date = date.splitlines()[-1]
199 # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
200 # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
201 # -like" string, which we must then edit to make compliant), because
202 # it's been around since git-1.5.3, and it's too difficult to
203 # discover which version we're using, or to work around using an
204 # older one.
205 date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
206 refnames = keywords["refnames"].strip()
207 if refnames.startswith("$Format"):
208 if verbose:
209 print("keywords are unexpanded, not using")
210 raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
211 refs = {r.strip() for r in refnames.strip("()").split(",")}
212 # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
213 # just "foo-1.0". If we see a "tag: " prefix, prefer those.
214 TAG = "tag: "
215 tags = {r[len(TAG):] for r in refs if r.startswith(TAG)}
216 if not tags:
217 # Either we're using git < 1.8.3, or there really are no tags. We use
218 # a heuristic: assume all version tags have a digit. The old git %d
219 # expansion behaves like git log --decorate=short and strips out the
220 # refs/heads/ and refs/tags/ prefixes that would let us distinguish
221 # between branches and tags. By ignoring refnames without digits, we
222 # filter out many common branch names like "release" and
223 # "stabilization", as well as "HEAD" and "master".
224 tags = {r for r in refs if re.search(r'\d', r)}
225 if verbose:
226 print("discarding '%s', no digits" % ",".join(refs - tags))
227 if verbose:
228 print("likely tags: %s" % ",".join(sorted(tags)))
229 for ref in sorted(tags):
230 # sorting will prefer e.g. "2.0" over "2.0rc1"
231 if ref.startswith(tag_prefix):
232 r = ref[len(tag_prefix):]
233 # Filter out refs that exactly match prefix or that don't start
234 # with a number once the prefix is stripped (mostly a concern
235 # when prefix is '')
236 if not re.match(r'\d', r):
237 continue
238 if verbose:
239 print("picking %s" % r)
240 return {"version": r,
241 "full-revisionid": keywords["full"].strip(),
242 "dirty": False, "error": None,
243 "date": date}
244 # no suitable tags, so version is "0+unknown", but full hex is still there
245 if verbose:
246 print("no suitable tags, using unknown + full revision id")
247 return {"version": "0+unknown",
248 "full-revisionid": keywords["full"].strip(),
249 "dirty": False, "error": "no suitable tags", "date": None}
252 @register_vcs_handler("git", "pieces_from_vcs")
253 def git_pieces_from_vcs(
254 tag_prefix: str,
255 root: str,
256 verbose: bool,
257 runner: Callable = run_command
258 ) -> Dict[str, Any]:
259 """Get version from 'git describe' in the root of the source tree.
261 This only gets called if the git-archive 'subst' keywords were *not*
262 expanded, and _version.py hasn't already been rewritten with a short
263 version string, meaning we're inside a checked out source tree.
265 GITS = ["git"]
266 if sys.platform == "win32":
267 GITS = ["git.cmd", "git.exe"]
269 # GIT_DIR can interfere with correct operation of Versioneer.
270 # It may be intended to be passed to the Versioneer-versioned project,
271 # but that should not change where we get our version from.
272 env = os.environ.copy()
273 env.pop("GIT_DIR", None)
274 runner = functools.partial(runner, env=env)
276 _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root,
277 hide_stderr=not verbose)
278 if rc != 0:
279 if verbose:
280 print("Directory %s not under git control" % root)
281 raise NotThisMethod("'git rev-parse --git-dir' returned error")
283 # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
284 # if there isn't one, this yields HEX[-dirty] (no NUM)
285 describe_out, rc = runner(GITS, [
286 "describe", "--tags", "--dirty", "--always", "--long",
287 "--match", f"{tag_prefix}[[:digit:]]*"
288 ], cwd=root)
289 # --long was added in git-1.5.5
290 if describe_out is None:
291 raise NotThisMethod("'git describe' failed")
292 describe_out = describe_out.strip()
293 full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root)
294 if full_out is None:
295 raise NotThisMethod("'git rev-parse' failed")
296 full_out = full_out.strip()
298 pieces: Dict[str, Any] = {}
299 pieces["long"] = full_out
300 pieces["short"] = full_out[:7] # maybe improved later
301 pieces["error"] = None
303 branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"],
304 cwd=root)
305 # --abbrev-ref was added in git-1.6.3
306 if rc != 0 or branch_name is None:
307 raise NotThisMethod("'git rev-parse --abbrev-ref' returned error")
308 branch_name = branch_name.strip()
310 if branch_name == "HEAD":
311 # If we aren't exactly on a branch, pick a branch which represents
312 # the current commit. If all else fails, we are on a branchless
313 # commit.
314 branches, rc = runner(GITS, ["branch", "--contains"], cwd=root)
315 # --contains was added in git-1.5.4
316 if rc != 0 or branches is None:
317 raise NotThisMethod("'git branch --contains' returned error")
318 branches = branches.split("\n")
320 # Remove the first line if we're running detached
321 if "(" in branches[0]:
322 branches.pop(0)
324 # Strip off the leading "* " from the list of branches.
325 branches = [branch[2:] for branch in branches]
326 if "master" in branches:
327 branch_name = "master"
328 elif not branches:
329 branch_name = None
330 else:
331 # Pick the first branch that is returned. Good or bad.
332 branch_name = branches[0]
334 pieces["branch"] = branch_name
336 # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
337 # TAG might have hyphens.
338 git_describe = describe_out
340 # look for -dirty suffix
341 dirty = git_describe.endswith("-dirty")
342 pieces["dirty"] = dirty
343 if dirty:
344 git_describe = git_describe[:git_describe.rindex("-dirty")]
346 # now we have TAG-NUM-gHEX or HEX
348 if "-" in git_describe:
349 # TAG-NUM-gHEX
350 mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
351 if not mo:
352 # unparsable. Maybe git-describe is misbehaving?
353 pieces["error"] = ("unable to parse git-describe output: '%s'"
354 % describe_out)
355 return pieces
357 # tag
358 full_tag = mo.group(1)
359 if not full_tag.startswith(tag_prefix):
360 if verbose:
361 fmt = "tag '%s' doesn't start with prefix '%s'"
362 print(fmt % (full_tag, tag_prefix))
363 pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
364 % (full_tag, tag_prefix))
365 return pieces
366 pieces["closest-tag"] = full_tag[len(tag_prefix):]
368 # distance: number of commits since tag
369 pieces["distance"] = int(mo.group(2))
371 # commit: short hex revision ID
372 pieces["short"] = mo.group(3)
374 else:
375 # HEX: no tags
376 pieces["closest-tag"] = None
377 out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root)
378 pieces["distance"] = len(out.split()) # total number of commits
380 # commit date: see ISO-8601 comment in git_versions_from_keywords()
381 date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip()
382 # Use only the last line. Previous lines may contain GPG signature
383 # information.
384 date = date.splitlines()[-1]
385 pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
387 return pieces
390 def plus_or_dot(pieces: Dict[str, Any]) -> str:
391 """Return a + if we don't already have one, else return a ."""
392 if "+" in pieces.get("closest-tag", ""):
393 return "."
394 return "+"
397 def render_pep440(pieces: Dict[str, Any]) -> str:
398 """Build up version string, with post-release "local version identifier".
400 Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
401 get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
403 Exceptions:
404 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
406 if pieces["closest-tag"]:
407 rendered = pieces["closest-tag"]
408 if pieces["distance"] or pieces["dirty"]:
409 rendered += plus_or_dot(pieces)
410 rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
411 if pieces["dirty"]:
412 rendered += ".dirty"
413 else:
414 # exception #1
415 rendered = "0+untagged.%d.g%s" % (pieces["distance"],
416 pieces["short"])
417 if pieces["dirty"]:
418 rendered += ".dirty"
419 return rendered
422 def render_pep440_branch(pieces: Dict[str, Any]) -> str:
423 """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] .
425 The ".dev0" means not master branch. Note that .dev0 sorts backwards
426 (a feature branch will appear "older" than the master branch).
428 Exceptions:
429 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty]
431 if pieces["closest-tag"]:
432 rendered = pieces["closest-tag"]
433 if pieces["distance"] or pieces["dirty"]:
434 if pieces["branch"] != "master":
435 rendered += ".dev0"
436 rendered += plus_or_dot(pieces)
437 rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
438 if pieces["dirty"]:
439 rendered += ".dirty"
440 else:
441 # exception #1
442 rendered = "0"
443 if pieces["branch"] != "master":
444 rendered += ".dev0"
445 rendered += "+untagged.%d.g%s" % (pieces["distance"],
446 pieces["short"])
447 if pieces["dirty"]:
448 rendered += ".dirty"
449 return rendered
452 def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]:
453 """Split pep440 version string at the post-release segment.
455 Returns the release segments before the post-release and the
456 post-release version number (or -1 if no post-release segment is present).
458 vc = str.split(ver, ".post")
459 return vc[0], int(vc[1] or 0) if len(vc) == 2 else None
462 def render_pep440_pre(pieces: Dict[str, Any]) -> str:
463 """TAG[.postN.devDISTANCE] -- No -dirty.
465 Exceptions:
466 1: no tags. 0.post0.devDISTANCE
468 if pieces["closest-tag"]:
469 if pieces["distance"]:
470 # update the post release segment
471 tag_version, post_version = pep440_split_post(pieces["closest-tag"])
472 rendered = tag_version
473 if post_version is not None:
474 rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"])
475 else:
476 rendered += ".post0.dev%d" % (pieces["distance"])
477 else:
478 # no commits, use the tag as the version
479 rendered = pieces["closest-tag"]
480 else:
481 # exception #1
482 rendered = "0.post0.dev%d" % pieces["distance"]
483 return rendered
486 def render_pep440_post(pieces: Dict[str, Any]) -> str:
487 """TAG[.postDISTANCE[.dev0]+gHEX] .
489 The ".dev0" means dirty. Note that .dev0 sorts backwards
490 (a dirty tree will appear "older" than the corresponding clean one),
491 but you shouldn't be releasing software with -dirty anyways.
493 Exceptions:
494 1: no tags. 0.postDISTANCE[.dev0]
496 if pieces["closest-tag"]:
497 rendered = pieces["closest-tag"]
498 if pieces["distance"] or pieces["dirty"]:
499 rendered += ".post%d" % pieces["distance"]
500 if pieces["dirty"]:
501 rendered += ".dev0"
502 rendered += plus_or_dot(pieces)
503 rendered += "g%s" % pieces["short"]
504 else:
505 # exception #1
506 rendered = "0.post%d" % pieces["distance"]
507 if pieces["dirty"]:
508 rendered += ".dev0"
509 rendered += "+g%s" % pieces["short"]
510 return rendered
513 def render_pep440_post_branch(pieces: Dict[str, Any]) -> str:
514 """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] .
516 The ".dev0" means not master branch.
518 Exceptions:
519 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty]
521 if pieces["closest-tag"]:
522 rendered = pieces["closest-tag"]
523 if pieces["distance"] or pieces["dirty"]:
524 rendered += ".post%d" % pieces["distance"]
525 if pieces["branch"] != "master":
526 rendered += ".dev0"
527 rendered += plus_or_dot(pieces)
528 rendered += "g%s" % pieces["short"]
529 if pieces["dirty"]:
530 rendered += ".dirty"
531 else:
532 # exception #1
533 rendered = "0.post%d" % pieces["distance"]
534 if pieces["branch"] != "master":
535 rendered += ".dev0"
536 rendered += "+g%s" % pieces["short"]
537 if pieces["dirty"]:
538 rendered += ".dirty"
539 return rendered
542 def render_pep440_old(pieces: Dict[str, Any]) -> str:
543 """TAG[.postDISTANCE[.dev0]] .
545 The ".dev0" means dirty.
547 Exceptions:
548 1: no tags. 0.postDISTANCE[.dev0]
550 if pieces["closest-tag"]:
551 rendered = pieces["closest-tag"]
552 if pieces["distance"] or pieces["dirty"]:
553 rendered += ".post%d" % pieces["distance"]
554 if pieces["dirty"]:
555 rendered += ".dev0"
556 else:
557 # exception #1
558 rendered = "0.post%d" % pieces["distance"]
559 if pieces["dirty"]:
560 rendered += ".dev0"
561 return rendered
564 def render_git_describe(pieces: Dict[str, Any]) -> str:
565 """TAG[-DISTANCE-gHEX][-dirty].
567 Like 'git describe --tags --dirty --always'.
569 Exceptions:
570 1: no tags. HEX[-dirty] (note: no 'g' prefix)
572 if pieces["closest-tag"]:
573 rendered = pieces["closest-tag"]
574 if pieces["distance"]:
575 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
576 else:
577 # exception #1
578 rendered = pieces["short"]
579 if pieces["dirty"]:
580 rendered += "-dirty"
581 return rendered
584 def render_git_describe_long(pieces: Dict[str, Any]) -> str:
585 """TAG-DISTANCE-gHEX[-dirty].
587 Like 'git describe --tags --dirty --always -long'.
588 The distance/hash is unconditional.
590 Exceptions:
591 1: no tags. HEX[-dirty] (note: no 'g' prefix)
593 if pieces["closest-tag"]:
594 rendered = pieces["closest-tag"]
595 rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
596 else:
597 # exception #1
598 rendered = pieces["short"]
599 if pieces["dirty"]:
600 rendered += "-dirty"
601 return rendered
604 def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]:
605 """Render the given version pieces into the requested style."""
606 if pieces["error"]:
607 return {"version": "unknown",
608 "full-revisionid": pieces.get("long"),
609 "dirty": None,
610 "error": pieces["error"],
611 "date": None}
613 if not style or style == "default":
614 style = "pep440" # the default
616 if style == "pep440":
617 rendered = render_pep440(pieces)
618 elif style == "pep440-branch":
619 rendered = render_pep440_branch(pieces)
620 elif style == "pep440-pre":
621 rendered = render_pep440_pre(pieces)
622 elif style == "pep440-post":
623 rendered = render_pep440_post(pieces)
624 elif style == "pep440-post-branch":
625 rendered = render_pep440_post_branch(pieces)
626 elif style == "pep440-old":
627 rendered = render_pep440_old(pieces)
628 elif style == "git-describe":
629 rendered = render_git_describe(pieces)
630 elif style == "git-describe-long":
631 rendered = render_git_describe_long(pieces)
632 else:
633 raise ValueError("unknown style '%s'" % style)
635 return {"version": rendered, "full-revisionid": pieces["long"],
636 "dirty": pieces["dirty"], "error": None,
637 "date": pieces.get("date")}
640 def get_versions() -> Dict[str, Any]:
641 """Get version information or return default if unable to do so."""
642 # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
643 # __file__, we can work backwards from there to the root. Some
644 # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
645 # case we can only use expanded keywords.
647 cfg = get_config()
648 verbose = cfg.verbose
650 try:
651 return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
652 verbose)
653 except NotThisMethod:
654 pass
656 try:
657 root = os.path.realpath(__file__)
658 # versionfile_source is the relative path from the top of the source
659 # tree (where the .git directory might live) to this file. Invert
660 # this to find the root from __file__.
661 for _ in cfg.versionfile_source.split('/'):
662 root = os.path.dirname(root)
663 except NameError:
664 return {"version": "0+unknown", "full-revisionid": None,
665 "dirty": None,
666 "error": "unable to find root of source tree",
667 "date": None}
669 try:
670 pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
671 return render(pieces, cfg.style)
672 except NotThisMethod:
673 pass
675 try:
676 if cfg.parentdir_prefix:
677 return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
678 except NotThisMethod:
679 pass
681 return {"version": "0+unknown", "full-revisionid": None,
682 "dirty": None,
683 "error": "unable to compute version", "date": None}