Completed
Push — develop ( 3606a5...c5328e )
by Kale
01:06
created

auxlib.write_version_into_init()   C

Complexity

Conditions 9

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 9
dl 0
loc 14
rs 6.4615
1
# -*- coding: utf-8 -*-
2
"""
3
=====
4
Usage
5
=====
6
7
Method #1: auxlib.packaging as a run time dependency
8
---------------------------------------------------
9
10
Place the following lines in your package's main __init__.py
11
12
from auxlib import get_version
13
__version__ = get_version(__file__, __package__)
14
15
16
17
Method #2: auxlib.packaging as a build time-only dependency
18
----------------------------------------------------------
19
20
21
import auxlib
22
23
# When executing the setup.py, we need to be able to import ourselves, this
24
# means that we need to add the src directory to the sys.path.
25
here = os.path.abspath(os.path.dirname(__file__))
26
src_dir = os.path.join(here, "auxlib")
27
sys.path.insert(0, src_dir)
28
29
setup(
30
    version=auxlib.__version__,
31
    cmdclass={
32
        'build_py': auxlib.BuildPyCommand,
33
        'sdist': auxlib.SDistCommand,
34
        'test': auxlib.Tox,
35
    },
36
)
37
38
39
40
Place the following lines in your package's main __init__.py
41
42
from auxlib import get_version
43
__version__ = get_version(__file__, __package__)
44
45
46
Method #3: write .version file
47
------------------------------
48
49
50
51
Configuring `python setup.py test` for Tox
52
------------------------------------------
53
54
must use setuptools (distutils doesn't have a test cmd)
55
56
setup(
57
    version=auxlib.__version__,
58
    cmdclass={
59
        'build_py': auxlib.BuildPyCommand,
60
        'sdist': auxlib.SDistCommand,
61
        'test': auxlib.Tox,
62
    },
63
)
64
65
66
"""
67
from __future__ import print_function, division, absolute_import
68
from logging import getLogger
69
from os import remove
70
from os.path import isdir, isfile, join
71
from re import match
72
try:
73
    from setuptools.command.build_py import build_py
74
    from setuptools.command.sdist import sdist
75
    from setuptools.command.test import test as TestCommand
76
except ImportError:
77
    from distutils.command.build_py import build_py
78
    from distutils.command.sdist import sdist
79
    TestCommand = object
80
81
from subprocess import CalledProcessError, check_call, check_output, call
82
import sys
83
84
from .path import absdirname, PackageFile
85
86
log = getLogger(__name__)
87
88
__all__ = ["get_version", "BuildPyCommand", "SDistCommand", "Tox", "is_git_repo"]
89
90
91
def _get_version_from_pkg_info(package_name):
92
    with PackageFile('.version', package_name) as fh:
93
        return fh.read()
94
95
96
def _is_git_dirty(path):
97
    try:
98
        check_call(('git', 'diff', '--quiet'), cwd=path)
99
        check_call(('git', 'diff', '--cached', '--quiet'), cwd=path)
100
        return False
101
    except CalledProcessError:
102
        return True
103
104
105
def _get_most_recent_git_tag(path):
106
    try:
107
        return check_output(("git", "describe", "--tags"), cwd=path).strip()
108
    except CalledProcessError as e:
109
        if e.returncode == 128:
110
            return "0.0.0.0"
111
        else:
112
            raise  # pragma: no cover
113
114
115
def _get_git_hash(path):
116
    try:
117
        return check_output(("git", "rev-parse", "HEAD"), cwd=path).strip()[:7]
118
    except CalledProcessError:
119
        return 0
120
121
122
def _get_version_from_git_tag(path):
123
    """Return a PEP-440 compliant version derived from the git status.
124
    If that fails for any reason, return the first 7 chars of the changeset hash.
125
    """
126
    tag = _get_most_recent_git_tag(path)
127
    m = match(b"(?P<xyz>\d+\.\d+\.\d+)(?:-(?P<dev>\d+)-(?P<hash>.+))?", tag)
128
    version = m.group('xyz').decode('utf-8')
129
    if m.group('dev') or _is_git_dirty(path):
130
        dev = (m.group('dev') or b'0').decode('utf-8')
131
        hash_ = (m.group('hash') or _get_git_hash(path)).decode('utf-8')
132
        version += ".dev{dev}+{hash_}".format(dev=dev, hash_=hash_)
133
    return version
134
135
136
def is_git_repo(path):
137
    return call(('git', 'rev-parse'), cwd=path) == 0
138
139
140
def get_version(file, package):
141
    """Returns a version string for the current package, derived
142
    either from git or from a .version file.
143
144
    This function is expected to run in two contexts. In a development
145
    context, where .git/ exists, the version is pulled from git tags.
146
    Using the BuildPyCommand and SDistCommand classes for cmdclass in
147
    setup.py will write a .version file into any dist.
148
149
    In an installed context, the .version file written at dist build
150
    time is the source of version information.
151
152
    """
153
    try:
154
        # first check for .version file
155
        version_from_pkg = _get_version_from_pkg_info(package)
156
        return (version_from_pkg.decode('UTF-8')
157
                if hasattr(version_from_pkg, 'decode')
158
                else version_from_pkg)
159
    except IOError:
160
        # no .version file found; fall back to git repo
161
        here = absdirname(file)
162
        if is_git_repo(here):
163
            return _get_version_from_git_tag(here)
164
165
    raise RuntimeError("Could not get package version (no .git or .version file)")
166
167
168
def write_version_into_init(target_dir, version):
169
    target_init_file = join(target_dir, "__init__.py")
170
    assert isfile(target_init_file), "File not found: {0}".format(target_init_file)
171
    with open(target_init_file, 'r') as f:
172
        init_lines = f.readlines()
173
    for q in range(len(init_lines)):
174
        if init_lines[q].startswith('__version__'):
175
            init_lines[q] = '__version__ = "{0}"\n'.format(version)
176
        elif init_lines[q].startswith(('from auxlib', 'import auxlib')):
177
            init_lines[q] = None
178
    print("UPDATING {0}".format(target_init_file))
179
    remove(target_init_file)
180
    with open(target_init_file, 'w') as f:
181
        f.write(''.join(l for l in init_lines if l is not None))
182
183
184
def write_version_file(target_dir, version):
185
    assert isdir(target_dir), "Directory not found: {0}".format(target_dir)
186
    target_file = join(target_dir, ".version")
187
    with open(target_file, 'w') as f:
188
        f.write(version)
189
190
191
class BuildPyCommand(build_py):
192
    def run(self):
193
        build_py.run(self)
194
        target_dir = join(self.build_lib, self.distribution.metadata.name)
195
        write_version_into_init(target_dir, self.distribution.metadata.version)
196
        write_version_file(target_dir, self.distribution.metadata.version)
197
        # TODO: separate out .version file implementation
198
199
200
class SDistCommand(sdist):
201
    def make_release_tree(self, base_dir, files):
202
        sdist.make_release_tree(self, base_dir, files)
203
        target_dir = join(base_dir, self.distribution.metadata.name)
204
        write_version_into_init(target_dir, self.distribution.metadata.version)
205
        write_version_file(target_dir, self.distribution.metadata.version)
206
207
208
class Tox(TestCommand):
209
    """
210
    TODO:
211
        - Make this class inherit from distutils instead of setuptools
212
    """
213
    user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
214
215
    def initialize_options(self):
216
        TestCommand.initialize_options(self)
217
        self.tox_args = None
218
219
    def finalize_options(self):
220
        TestCommand.finalize_options(self)
221
        self.test_args = []
222
        self.test_suite = True
223
224
    def run_tests(self):
225
        # import here, because outside the eggs aren't loaded
226
        import tox
227
        import shlex
228
        args = self.tox_args
229
        if args:
230
            args = shlex.split(self.tox_args)
231
        else:
232
            args = ''
233
        errno = tox.cmdline(args=args)
234
        sys.exit(errno)
235