Completed
Pull Request — develop (#11)
by Kale
01:10
created

get_version()   A

Complexity

Conditions 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 1
c 4
b 0
f 0
dl 0
loc 15
rs 9.4285
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__)
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__)
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
69
import sys
70
from collections import namedtuple
71
from logging import getLogger
72
from os import getenv, remove
73
from os.path import abspath, dirname, expanduser, isdir, isfile, join
74
from re import compile
75
from shlex import split
76
from subprocess import CalledProcessError, Popen, PIPE
77
try:
78
    from setuptools.command.build_py import build_py
79
    from setuptools.command.sdist import sdist
80
    from setuptools.command.test import test as TestCommand
81
except ImportError:
82
    from distutils.command.build_py import build_py
83
    from distutils.command.sdist import sdist
84
    TestCommand = object
85
86
87
log = getLogger(__name__)
88
89
Response = namedtuple('Response', ['stdout', 'stderr', 'rc'])
90
GIT_DESCRIBE_REGEX = compile(r"(?:[_-a-zA-Z]*)"
91
                             r"(?P<version>\d+\.\d+\.\d+)"
92
                             r"(?:-(?P<dev>\d+)-g(?P<hash>[0-9a-f]{7}))$")
93
94
95
def call(path, command, raise_on_error=True):
96
    p = Popen(split(command), cwd=path, stdout=PIPE, stderr=PIPE)
97
    stdout, stderr = p.communicate()
98
    rc = p.returncode
99
    log.debug("{0} $  {1}\n"
100
              "  stdout: {2}\n"
101
              "  stderr: {3}\n"
102
              "  rc: {4}"
103
              .format(path, command, stdout, stderr, rc))
104
    if raise_on_error and rc != 0:
105
        raise CalledProcessError(rc, command, "stdout: {0}\nstderr: {1}".format(stdout, stderr))
106
    return Response(stdout.decode('utf-8'), stderr.decode('utf-8'), int(rc))
107
108
109
def _get_version_from_version_file(path):
110
    file_path = join(path, '.version')
111
    if isfile(file_path):
112
        with open(file_path, 'r') as fh:
113
            return fh.read().strip()
114
115
116
def _git_describe_tags(path):
117
    call(path, "git update-index --refresh", raise_on_error=False)
118
    response = call(path, "git describe --tags --long", raise_on_error=False)
119
    if response.rc == 0:
120
        return response.stdout.strip()
121
    elif response.rc == 128 and "no names found" in response.stderr.lower():
122
        # directory is a git repo, but no tags found
123
        return None
124
    elif response.rc == 128 and "not a git repository" in response.stderr.lower():
125
        return None
126
    elif response.rc == 127:
127
        log.error("git not found on path: PATH={0}".format(getenv('PATH', None)))
128
        raise CalledProcessError(response.rc, response.stderr)
129
    else:
130
        raise CalledProcessError(response.rc, response.stderr)
131
132
133
def _get_version_from_git_tag(path):
134
    """Return a PEP440-compliant version derived from the git status.
135
    If that fails for any reason, return the first 7 chars of the changeset hash.
136
    """
137
    m = GIT_DESCRIBE_REGEX.match(_git_describe_tags(path) or '')
138
    if m is None:
139
        return None
140
    version, post_commit, hash = m.groups()
141
    if post_commit == 0:
142
        return version
143
    else:
144
        return "{0}.dev{1}+{2}".format(version, post_commit, hash)
145
146
147
def get_version(dunder_file):
148
    """Returns a version string for the current package, derived
149
    either from git or from a .version file.
150
151
    This function is expected to run in two contexts. In a development
152
    context, where .git/ exists, the version is pulled from git tags.
153
    Using the BuildPyCommand and SDistCommand classes for cmdclass in
154
    setup.py will write a .version file into any dist.
155
156
    In an installed context, the .version file written at dist build
157
    time is the source of version information.
158
159
    """
160
    path = abspath(expanduser(dirname(dunder_file)))
161
    return _get_version_from_version_file(path) or _get_version_from_git_tag(path)
162
163
164
def write_version_into_init(target_dir, version):
165
    target_init_file = join(target_dir, "__init__.py")
166
    assert isfile(target_init_file), "File not found: {0}".format(target_init_file)
167
    with open(target_init_file, 'r') as f:
168
        init_lines = f.readlines()
169
    for q in range(len(init_lines)):
170
        if init_lines[q].startswith('__version__'):
171
            init_lines[q] = '__version__ = "{0}"\n'.format(version)
172
        elif init_lines[q].startswith(('from auxlib', 'import auxlib')):
173
            init_lines[q] = None
174
    print("UPDATING {0}".format(target_init_file))
175
    remove(target_init_file)
176
    with open(target_init_file, 'w') as f:
177
        f.write(''.join(l for l in init_lines if l is not None))
178
179
180
def write_version_file(target_dir, version):
181
    assert isdir(target_dir), "Directory not found: {0}".format(target_dir)
182
    target_file = join(target_dir, ".version")
183
    with open(target_file, 'w') as f:
184
        f.write(version)
185
186
187
class BuildPyCommand(build_py):
188
    def run(self):
189
        build_py.run(self)
190
        target_dir = join(self.build_lib, self.distribution.metadata.name)
191
        write_version_into_init(target_dir, self.distribution.metadata.version)
192
        write_version_file(target_dir, self.distribution.metadata.version)
193
194
195
class SDistCommand(sdist):
196
    def make_release_tree(self, base_dir, files):
197
        sdist.make_release_tree(self, base_dir, files)
198
        target_dir = join(base_dir, self.distribution.metadata.name)
199
        write_version_into_init(target_dir, self.distribution.metadata.version)
200
        write_version_file(target_dir, self.distribution.metadata.version)
201
202
203
class Tox(TestCommand):
204
    user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
205
206
    def initialize_options(self):
207
        TestCommand.initialize_options(self)
208
        self.tox_args = None
209
210
    def finalize_options(self):
211
        TestCommand.finalize_options(self)
212
        self.test_args = []
213
        self.test_suite = True
214
215
    def run_tests(self):
216
        # import here, cause outside the eggs aren't loaded
217
        from tox import cmdline
218
        from shlex import split
219
        args = self.tox_args
220
        if args:
221
            args = split(self.tox_args)
222
        else:
223
            args = ''
224
        errno = cmdline(args=args)
225
        sys.exit(errno)
226