adaptive/_version.py
3a69cd99
 # This file is part of 'miniver': https://github.com/jbweston/miniver
 #
28e733fc
 import os
94a7301b
 import subprocess
176c745c
 from collections import namedtuple
 
eb17d6fe
 from setuptools.command.build_py import build_py as build_py_orig
94a7301b
 from setuptools.command.sdist import sdist as sdist_orig
28e733fc
 
716dbce8
 Version = namedtuple("Version", ("release", "dev", "labels"))
663e76da
 
28e733fc
 # No public API
 __all__ = []
 
 package_root = os.path.dirname(os.path.realpath(__file__))
273e1596
 package_name = os.path.basename(package_root)
28e733fc
 distr_root = os.path.dirname(package_root)
eb17d6fe
 # If the package is inside a "src" directory the
 # distribution root is 1 level up.
 if os.path.split(distr_root)[1] == "src":
     _package_root_inside_src = True
     distr_root = os.path.dirname(distr_root)
 else:
     _package_root_inside_src = False
28e733fc
 
716dbce8
 STATIC_VERSION_FILE = "_static_version.py"
28e733fc
 
94a7301b
 
28e733fc
 def get_version(version_file=STATIC_VERSION_FILE):
3a69cd99
     version_info = get_static_version_info(version_file)
716dbce8
     version = version_info["version"]
3a69cd99
     if version == "__use_git__":
28e733fc
         version = get_version_from_git()
         if not version:
             version = get_version_from_git_archive(version_info)
         if not version:
663e76da
             version = Version("unknown", None, None)
3a69cd99
         return pep440_format(version)
663e76da
     else:
         return version
 
 
3a69cd99
 def get_static_version_info(version_file=STATIC_VERSION_FILE):
     version_info = {}
716dbce8
     with open(os.path.join(package_root, version_file), "rb") as f:
3a69cd99
         exec(f.read(), {}, version_info)
     return version_info
 
 
 def version_is_from_git(version_file=STATIC_VERSION_FILE):
716dbce8
     return get_static_version_info(version_file)["version"] == "__use_git__"
3a69cd99
 
 
 def pep440_format(version_info):
663e76da
     release, dev, labels = version_info
 
     version_parts = [release]
     if dev:
716dbce8
         if release.endswith("-dev") or release.endswith(".dev"):
663e76da
             version_parts.append(dev)
3a69cd99
         else:  # prefer PEP440 over strict adhesion to semver
716dbce8
             version_parts.append(f".dev{dev}")
663e76da
 
     if labels:
716dbce8
         version_parts.append("+")
663e76da
         version_parts.append(".".join(labels))
 
     return "".join(version_parts)
28e733fc
 
 
 def get_version_from_git():
     try:
716dbce8
         p = subprocess.Popen(
             ["git", "rev-parse", "--show-toplevel"],
             cwd=distr_root,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
         )
28e733fc
     except OSError:
         return
     if p.wait() != 0:
         return
716dbce8
     if not os.path.samefile(p.communicate()[0].decode().rstrip("\n"), distr_root):
28e733fc
         # The top-level directory of the current Git repository is not the same
3a69cd99
         # as the root directory of the distribution: do not extract the
28e733fc
         # version from Git.
         return
 
     # git describe --first-parent does not take into account tags from branches
3a69cd99
     # that were merged-in. The '--long' flag gets us the 'dev' version and
     # git hash, '--always' returns the git hash even if there are no tags.
716dbce8
     for opts in [["--first-parent"], []]:
28e733fc
         try:
3a69cd99
             p = subprocess.Popen(
716dbce8
                 ["git", "describe", "--long", "--always"] + opts,
3a69cd99
                 cwd=distr_root,
716dbce8
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
             )
28e733fc
         except OSError:
             return
         if p.wait() == 0:
             break
     else:
         return
 
3a69cd99
     description = (
         p.communicate()[0]
         .decode()
716dbce8
         .strip("v")  # Tags can have a leading 'v', but the version should not
         .rstrip("\n")
         .rsplit("-", 2)  # Split the latest tag, commits since tag, and hash
3a69cd99
     )
 
     try:
         release, dev, git = description
     except ValueError:  # No tags, only the git hash
         # prepend 'g' to match with format returned by 'git describe'
716dbce8
         git = "g{}".format(*description)
         release = "unknown"
3a69cd99
         dev = None
 
28e733fc
     labels = []
663e76da
     if dev == "0":
         dev = None
     else:
28e733fc
         labels.append(git)
 
     try:
716dbce8
         p = subprocess.Popen(["git", "diff", "--quiet"], cwd=distr_root)
28e733fc
     except OSError:
716dbce8
         labels.append("confused")  # This should never happen.
28e733fc
     else:
         if p.wait() == 1:
716dbce8
             labels.append("dirty")
28e733fc
 
663e76da
     return Version(release, dev, labels)
28e733fc
 
 
 # TODO: change this logic when there is a git pretty-format
 #       that gives the same output as 'git describe'.
663e76da
 #       Currently we can only tell the tag the current commit is
 #       pointing to, or its hash (with no version info)
 #       if it is not tagged.
28e733fc
 def get_version_from_git_archive(version_info):
     try:
716dbce8
         refnames = version_info["refnames"]
         git_hash = version_info["git_hash"]
28e733fc
     except KeyError:
         # These fields are not present if we are running from an sdist.
         # Execution should never reach here, though
         return None
 
716dbce8
     if git_hash.startswith("$Format") or refnames.startswith("$Format"):
28e733fc
         # variables not expanded during 'git archive'
         return None
 
716dbce8
     VTAG = "tag: v"
176c745c
     refs = {r.strip() for r in refnames.split(",")}
716dbce8
     version_tags = {r[len(VTAG) :] for r in refs if r.startswith(VTAG)}
663e76da
     if version_tags:
         release, *_ = sorted(version_tags)  # prefer e.g. "2.0" over "2.0rc1"
         return Version(release, dev=None, labels=None)
28e733fc
     else:
716dbce8
         return Version("unknown", dev=None, labels=[f"g{git_hash}"])
3a69cd99
 
663e76da
 
3a69cd99
 __version__ = get_version()
28e733fc
 
273e1596
 
 # The following section defines a module global 'cmdclass',
 # which can be used from setup.py. The 'package_name' and
3a69cd99
 # '__version__' module globals are used (but not modified).
273e1596
 
716dbce8
 
273e1596
 def _write_version(fname):
     # This could be a hard link, so try to delete it first.  Is there any way
     # to do this atomically together with opening?
     try:
         os.remove(fname)
     except OSError:
         pass
716dbce8
     with open(fname, "w") as f:
         f.write(
             "# This file has been created by setup.py.\n"
             "version = '{}'\n".format(__version__)
         )
273e1596
 
 
3a69cd99
 class _build_py(build_py_orig):
273e1596
     def run(self):
         super().run()
716dbce8
         _write_version(os.path.join(self.build_lib, package_name, STATIC_VERSION_FILE))
273e1596
 
94a7301b
 
273e1596
 class _sdist(sdist_orig):
     def make_release_tree(self, base_dir, files):
         super().make_release_tree(base_dir, files)
eb17d6fe
         if _package_root_inside_src:
             p = os.path.join("src", package_name)
         else:
             p = package_name
         _write_version(os.path.join(base_dir, p, STATIC_VERSION_FILE))
94a7301b
 
 
3a69cd99
 cmdclass = dict(sdist=_sdist, build_py=_build_py)