miniver/_version.py
ec56e4e9
 # -*- coding: utf-8 -*-
 # This file is part of 'miniver': https://github.com/jbweston/miniver
 #
cde1ceab
 from collections import namedtuple
 import os
 import subprocess
 
5da21b21
 from setuptools.command.build_py import build_py as build_py_orig
cde1ceab
 from setuptools.command.sdist import sdist as sdist_orig
 
ef42cddf
 Version = namedtuple("Version", ("release", "dev", "labels"))
cde1ceab
 
 # No public API
 __all__ = []
 
 package_root = os.path.dirname(os.path.realpath(__file__))
 package_name = os.path.basename(package_root)
 distr_root = os.path.dirname(package_root)
e6b4930c
 # 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
cde1ceab
 
ef42cddf
 STATIC_VERSION_FILE = "_static_version.py"
cde1ceab
 
 
 def get_version(version_file=STATIC_VERSION_FILE):
91512b7a
     version_info = get_static_version_info(version_file)
ef42cddf
     version = version_info["version"]
fd1fb823
     if version == "__use_git__":
cde1ceab
         version = get_version_from_git()
         if not version:
             version = get_version_from_git_archive(version_info)
         if not version:
             version = Version("unknown", None, None)
af154c88
         return pep440_format(version)
cde1ceab
     else:
         return version
 
 
91512b7a
 def get_static_version_info(version_file=STATIC_VERSION_FILE):
     version_info = {}
ef42cddf
     with open(os.path.join(package_root, version_file), "rb") as f:
91512b7a
         exec(f.read(), {}, version_info)
     return version_info
 
 
815c1f3e
 def version_is_from_git(version_file=STATIC_VERSION_FILE):
ef42cddf
     return get_static_version_info(version_file)["version"] == "__use_git__"
815c1f3e
 
 
af154c88
 def pep440_format(version_info):
cde1ceab
     release, dev, labels = version_info
 
     version_parts = [release]
     if dev:
ef42cddf
         if release.endswith("-dev") or release.endswith(".dev"):
cde1ceab
             version_parts.append(dev)
2e929034
         else:  # prefer PEP440 over strict adhesion to semver
ef42cddf
             version_parts.append(".dev{}".format(dev))
cde1ceab
 
     if labels:
ef42cddf
         version_parts.append("+")
cde1ceab
         version_parts.append(".".join(labels))
 
     return "".join(version_parts)
 
 
 def get_version_from_git():
     try:
ef42cddf
         p = subprocess.Popen(
             ["git", "rev-parse", "--show-toplevel"],
             cwd=distr_root,
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
         )
cde1ceab
     except OSError:
         return
     if p.wait() != 0:
         return
ef42cddf
     if not os.path.samefile(p.communicate()[0].decode().rstrip("\n"), distr_root):
cde1ceab
         # The top-level directory of the current Git repository is not the same
57f61e3a
         # as the root directory of the distribution: do not extract the
cde1ceab
         # version from Git.
         return
 
     # git describe --first-parent does not take into account tags from branches
b8e538f5
     # 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.
ef42cddf
     for opts in [["--first-parent"], []]:
cde1ceab
         try:
3116e0a2
             p = subprocess.Popen(
ef42cddf
                 ["git", "describe", "--long", "--always"] + opts,
3116e0a2
                 cwd=distr_root,
ef42cddf
                 stdout=subprocess.PIPE,
                 stderr=subprocess.PIPE,
             )
cde1ceab
         except OSError:
             return
         if p.wait() == 0:
             break
     else:
         return
 
0a22aef7
     description = (
         p.communicate()[0]
b8e538f5
         .decode()
ef42cddf
         .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
0a22aef7
     )
b8e538f5
 
     try:
         release, dev, git = description
     except ValueError:  # No tags, only the git hash
2a6f707c
         # prepend 'g' to match with format returned by 'git describe'
ef42cddf
         git = "g{}".format(*description)
         release = "unknown"
b8e538f5
         dev = None
 
cde1ceab
     labels = []
     if dev == "0":
         dev = None
     else:
         labels.append(git)
 
     try:
ef42cddf
         p = subprocess.Popen(["git", "diff", "--quiet"], cwd=distr_root)
cde1ceab
     except OSError:
ef42cddf
         labels.append("confused")  # This should never happen.
cde1ceab
     else:
         if p.wait() == 1:
ef42cddf
             labels.append("dirty")
cde1ceab
 
     return Version(release, dev, labels)
 
 
 # TODO: change this logic when there is a git pretty-format
 #       that gives the same output as 'git describe'.
 #       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.
 def get_version_from_git_archive(version_info):
     try:
ef42cddf
         refnames = version_info["refnames"]
         git_hash = version_info["git_hash"]
cde1ceab
     except KeyError:
         # These fields are not present if we are running from an sdist.
         # Execution should never reach here, though
         return None
 
ef42cddf
     if git_hash.startswith("$Format") or refnames.startswith("$Format"):
cde1ceab
         # variables not expanded during 'git archive'
         return None
 
ef42cddf
     VTAG = "tag: v"
cde1ceab
     refs = set(r.strip() for r in refnames.split(","))
ef42cddf
     version_tags = set(r[len(VTAG) :] for r in refs if r.startswith(VTAG))
cde1ceab
     if version_tags:
         release, *_ = sorted(version_tags)  # prefer e.g. "2.0" over "2.0rc1"
         return Version(release, dev=None, labels=None)
     else:
ef42cddf
         return Version("unknown", dev=None, labels=["g{}".format(git_hash)])
cde1ceab
 
 
8dddbd2e
 __version__ = get_version()
cde1ceab
 
3116e0a2
 
cde1ceab
 # The following section defines a module global 'cmdclass',
 # which can be used from setup.py. The 'package_name' and
8dddbd2e
 # '__version__' module globals are used (but not modified).
cde1ceab
 
ef42cddf
 
cde1ceab
 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
ef42cddf
     with open(fname, "w") as f:
         f.write(
             "# This file has been created by setup.py.\n"
             "version = '{}'\n".format(__version__)
         )
cde1ceab
 
 
02e9fdc0
 class _build_py(build_py_orig):
cde1ceab
     def run(self):
         super().run()
ef42cddf
         _write_version(os.path.join(self.build_lib, package_name, STATIC_VERSION_FILE))
cde1ceab
 
 
 class _sdist(sdist_orig):
     def make_release_tree(self, base_dir, files):
         super().make_release_tree(base_dir, files)
e6b4930c
         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))
cde1ceab
 
 
02e9fdc0
 cmdclass = dict(sdist=_sdist, build_py=_build_py)