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