diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..ca3ca1e0e201a93c23ac2e1348eb6c9f0b2e1486 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +mumpy/_static_version.py export-subst \ No newline at end of file diff --git a/mumpy/__init__.py b/mumpy/__init__.py index 195f661b7a95138866667f852ac0c3d8adf9f011..6211b3078fc910fa6781d9d1a9b442accfc2e710 100644 --- a/mumpy/__init__.py +++ b/mumpy/__init__.py @@ -1 +1,2 @@ from .mumpy import * +from ._version import __version__ diff --git a/mumpy/_static_version.py b/mumpy/_static_version.py new file mode 100644 index 0000000000000000000000000000000000000000..5557f9b53452d608e449425b75c13912d54f4118 --- /dev/null +++ b/mumpy/_static_version.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +# This file will be overwritten by setup.py when a source or binary +# distribution is made. The magic value "__use_git__" is interpreted by +# version.py. + +version = "__use_git__" + +# These values are only set if the distribution was created with 'git archive' +refnames = "$Format:%D$" +git_hash = "$Format:%h$" diff --git a/mumpy/_version.py b/mumpy/_version.py new file mode 100644 index 0000000000000000000000000000000000000000..5319eaf83b3d8e820e4ee9ad0d0523fe871e71a9 --- /dev/null +++ b/mumpy/_version.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +from collections import namedtuple +import os +import subprocess + +from distutils.command.build_py import build_py as build_py_orig +from setuptools.command.sdist import sdist as sdist_orig + +Version = namedtuple('Version', ('release', 'dev', 'labels')) + +# 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) + +STATIC_VERSION_FILE = '_static_version.py' + + +def get_version(version_file=STATIC_VERSION_FILE): + version_info = {} + with open(os.path.join(package_root, version_file), 'rb') as f: + exec(f.read(), {}, version_info) + version = version_info['version'] + if version == "__use_git__": + version = get_version_from_git() + if not version: + version = get_version_from_git_archive(version_info) + if not version: + version = Version("unknown", None, None) + return pep440_format(version) + else: + return version + + +def pep440_format(version_info): + release, dev, labels = version_info + + version_parts = [release] + if dev: + if release.endswith('-dev') or release.endswith('.dev'): + version_parts.append(dev) + else: # prefer PEP440 over stric adhesion to semver + version_parts.append('.dev{}'.format(dev)) + + if labels: + version_parts.append('+') + version_parts.append(".".join(labels)) + + return "".join(version_parts) + + +def get_version_from_git(): + try: + p = subprocess.Popen(['git', 'rev-parse', '--show-toplevel'], + cwd=distr_root, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError: + return + if p.wait() != 0: + return + if not os.path.samefile(p.communicate()[0].decode().rstrip('\n'), + distr_root): + # The top-level directory of the current Git repository is not the same + # as the root directory of the distribution: do not extract the + # version from Git. + return + + # git describe --first-parent does not take into account tags from branches + # 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. + for opts in [['--first-parent'], []]: + try: + p = subprocess.Popen( + ['git', 'describe', '--long', '--always'] + opts, + cwd=distr_root, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError: + return + if p.wait() == 0: + break + else: + return + + description = (p.communicate()[0] + .decode() + .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 + + try: + release, dev, git = description + except ValueError: # No tags, only the git hash + # prepend 'g' to match with format returned by 'git describe' + git = 'g{}'.format(*description) + release = 'unknown' + dev = None + + labels = [] + if dev == "0": + dev = None + else: + labels.append(git) + + try: + p = subprocess.Popen(['git', 'diff', '--quiet'], cwd=distr_root) + except OSError: + labels.append('confused') # This should never happen. + else: + if p.wait() == 1: + labels.append('dirty') + + 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: + refnames = version_info['refnames'] + git_hash = version_info['git_hash'] + except KeyError: + # These fields are not present if we are running from an sdist. + # Execution should never reach here, though + return None + + if git_hash.startswith('$Format') or refnames.startswith('$Format'): + # variables not expanded during 'git archive' + return None + + VTAG = 'tag: v' + refs = set(r.strip() for r in refnames.split(",")) + version_tags = set(r[len(VTAG):] for r in refs if r.startswith(VTAG)) + if version_tags: + release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1" + return Version(release, dev=None, labels=None) + else: + return Version('unknown', dev=None, labels=['g{}'.format(git_hash)]) + + +__version__ = get_version() + + +# The following section defines a module global 'cmdclass', +# which can be used from setup.py. The 'package_name' and +# '__version__' module globals are used (but not modified). + +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 + with open(fname, 'w') as f: + f.write("# This file has been created by setup.py.\n" + "version = '{}'\n".format(__version__)) + + +class _build_py(build_py_orig): + def run(self): + super().run() + _write_version(os.path.join(self.build_lib, package_name, + STATIC_VERSION_FILE)) + + +class _sdist(sdist_orig): + def make_release_tree(self, base_dir, files): + super().make_release_tree(base_dir, files) + _write_version(os.path.join(base_dir, package_name, + STATIC_VERSION_FILE)) + + +cmdclass = dict(sdist=_sdist, build_py=_build_py) diff --git a/setup.py b/setup.py index 6937826cea143e19bb902fca4f956a33eace6674..bde7324cf79333421eacf9cde4199b7b701a6885 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ import configparser import collections from setuptools import setup, find_packages, Extension from distutils.command.build import build -from setuptools.command.sdist import sdist from setuptools.command.build_ext import build_ext import jinja2 as j2 @@ -184,6 +183,17 @@ def configure_special_extensions(exts, build_summary): return exts +# Loads version.py module without importing the whole package. +def get_version_and_cmdclass(package_path): + import os + from importlib.util import module_from_spec, spec_from_file_location + spec = spec_from_file_location('version', + os.path.join(package_path, '_version.py')) + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module.__version__, module.cmdclass + + def main(): mumps = {'mumpy.mumps': dict(sources=['mumpy/mumps.pyx'], @@ -215,6 +225,8 @@ def main(): language_level=3, compiler_directives={'linetrace': True}) + version, cmdclass = get_version_and_cmdclass('mumpy') + classifiers = """\ Development Status :: 3 - Alpha Intended Audience :: Science/Research @@ -227,7 +239,7 @@ def main(): Operating System :: Microsoft :: Windows""" setup(name='mumpy', - version='0.1.0', + version=version, author='mumpy authors', author_email='authors@kwant-project.org', description=("Python bindings for MUMPS "), @@ -235,9 +247,11 @@ def main(): url="https://gitlab.kwant-project.org/kwant/mumpy", license="BSD", packages=find_packages('.'), - cmdclass={'build': build, - 'sdist': sdist, - 'build_ext': build_ext}, + cmdclass={ + 'build': build, + 'build_ext': build_ext, + **cmdclass, + }, ext_modules=mumps, install_requires=['numpy'], classifiers=[c.strip() for c in classifiers.split('\n')])