Tox   A
last analyzed

Complexity

Total Complexity 4

Size/Duplication

Total Lines 24
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 24
rs 10
wmc 4

3 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize_options() 0 3 1
A run_tests() 0 11 2
A finalize_options() 0 4 1
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, listdir
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
from fnmatch import fnmatchcase
78
from distutils.util import convert_path
79
80
try:
81
    from setuptools.command.build_py import build_py
82
    from setuptools.command.sdist import sdist
83
    from setuptools.command.test import test as TestCommand
84
except ImportError:
85
    from distutils.command.build_py import build_py
86
    from distutils.command.sdist import sdist
87
88
    TestCommand = object
89
90
log = getLogger(__name__)
91
92
Response = namedtuple('Response', ['stdout', 'stderr', 'rc'])
93
GIT_DESCRIBE_REGEX = compile(r"(?:[_-a-zA-Z]*)"
94
                             r"(?P<version>[a-zA-Z0-9.]+)"
95
                             r"(?:-(?P<post>\d+)-g(?P<hash>[0-9a-f]{7,}))$")
96
97
98
def call(command, path=None, raise_on_error=True):
99
    path = sys.prefix if path is None else abspath(path)
100
    p = Popen(split(command), cwd=path, stdout=PIPE, stderr=PIPE)
101
    stdout, stderr = p.communicate()
102
    rc = p.returncode
103
    log.debug("{0} $  {1}\n"
104
              "  stdout: {2}\n"
105
              "  stderr: {3}\n"
106
              "  rc: {4}"
107
              .format(path, command, stdout, stderr, rc))
108
    if raise_on_error and rc != 0:
109
        raise CalledProcessError(rc, command, "stdout: {0}\nstderr: {1}".format(stdout, stderr))
110
    return Response(stdout.decode('utf-8'), stderr.decode('utf-8'), int(rc))
111
112
113
def _get_version_from_version_file(path):
114
    file_path = join(path, '.version')
115
    if isfile(file_path):
116
        with open(file_path, 'r') as fh:
117
            return fh.read().strip()
118
119
120
def _git_describe_tags(path):
121
    try:
122
        call("git update-index --refresh", path, raise_on_error=False)
123
    except CalledProcessError as e:
124
        # git is probably not installed
125
        log.warn(repr(e))
126
        return None
127
    response = call("git describe --tags --long", path, raise_on_error=False)
128
    if response.rc == 0:
129
        return response.stdout.strip()
130
    elif response.rc == 128 and "no names found" in response.stderr.lower():
131
        # directory is a git repo, but no tags found
132
        return None
133
    elif response.rc == 128 and "not a git repository" in response.stderr.lower():
134
        return None
135
    elif response.rc == 127:
136
        log.error("git not found on path: PATH={0}".format(getenv('PATH', None)))
137
        raise CalledProcessError(response.rc, response.stderr)
138
    else:
139
        raise CalledProcessError(response.rc, response.stderr)
140
141
142
def _get_version_from_git_tag(path):
143
    """Return a PEP440-compliant version derived from the git status.
144
    If that fails for any reason, return the changeset hash.
145
    """
146
    m = GIT_DESCRIBE_REGEX.match(_git_describe_tags(path) or '')
147
    if m is None:
148
        return None
149
    version, post_commit, hash = m.groups()
150
    return version if post_commit == '0' else "{0}.post{1}+{2}".format(version, post_commit, hash)
151
152
153
def get_version(dunder_file):
154
    """Returns a version string for the current package, derived
155
    either from git or from a .version file.
156
157
    This function is expected to run in two contexts. In a development
158
    context, where .git/ exists, the version is pulled from git tags.
159
    Using the BuildPyCommand and SDistCommand classes for cmdclass in
160
    setup.py will write a .version file into any dist.
161
162
    In an installed context, the .version file written at dist build
163
    time is the source of version information.
164
165
    """
166
    path = abspath(expanduser(dirname(dunder_file)))
167
    try:
168
        return _get_version_from_version_file(path) or _get_version_from_git_tag(path)
169
    except CalledProcessError as e:
170
        log.warn(repr(e))
171
        return None
172
    except Exception as e:
173
        log.exception(e)
174
        return None
175
176
177
def write_version_into_init(target_dir, version):
178
    target_init_file = join(target_dir, "__init__.py")
179
    assert isfile(target_init_file), "File not found: {0}".format(target_init_file)
180
    with open(target_init_file, 'r') as f:
181
        init_lines = f.readlines()
182
    for q in range(len(init_lines)):
183
        if init_lines[q].startswith('__version__'):
184
            init_lines[q] = '__version__ = "{0}"\n'.format(version)
185
        elif (init_lines[q].startswith(('from auxlib', 'import auxlib'))
186
              or 'auxlib.packaging' in init_lines[q]):
187
            init_lines[q] = None
188
    print("UPDATING {0}".format(target_init_file))
189
    remove(target_init_file)
190
    with open(target_init_file, 'w') as f:
191
        f.write(''.join(l for l in init_lines if l is not None))
192
193
194
def write_version_file(target_dir, version):
195
    assert isdir(target_dir), "Directory not found: {0}".format(target_dir)
196
    target_file = join(target_dir, ".version")
197
    print("WRITING {0} with version {1}".format(target_file, version))
198
    with open(target_file, 'w') as f:
199
        f.write(version)
200
201
202
class BuildPyCommand(build_py):
203
    def run(self):
204
        build_py.run(self)
205
        target_dir = join(self.build_lib, self.distribution.metadata.name)
206
        write_version_into_init(target_dir, self.distribution.metadata.version)
207
        write_version_file(target_dir, self.distribution.metadata.version)
208
        # TODO: separate out .version file implementation
209
210
211
class SDistCommand(sdist):
212
    def make_release_tree(self, base_dir, files):
213
        sdist.make_release_tree(self, base_dir, files)
214
        target_dir = join(base_dir, self.distribution.metadata.name)
215
        write_version_into_init(target_dir, self.distribution.metadata.version)
216
        write_version_file(target_dir, self.distribution.metadata.version)
217
218
219
class Tox(TestCommand):
220
    # TODO: Make this class inherit from distutils instead of setuptools
221
    user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
222
223
    def initialize_options(self):
224
        TestCommand.initialize_options(self)
225
        self.tox_args = None
226
227
    def finalize_options(self):
228
        TestCommand.finalize_options(self)
229
        self.test_args = []
230
        self.test_suite = True
231
232
    def run_tests(self):
233
        # import here, because outside the eggs aren't loaded
234
        from tox import cmdline
235
        from shlex import split
236
        args = self.tox_args
237
        if args:
238
            args = split(self.tox_args)
239
        else:
240
            args = ''
241
        errno = cmdline(args=args)
242
        sys.exit(errno)
243
244
245
# swiped from setuptools
246
def find_packages(where='.', exclude=()):
247
    out = []
248
    stack = [(convert_path(where), '')]
249
    while stack:
250
        where, prefix = stack.pop(0)
251
        for name in listdir(where):
252
            fn = join(where, name)
253
            if ('.' not in name and isdir(fn) and
254
                    isfile(join(fn, '__init__.py'))
255
                ):
256
                out.append(prefix + name)
257
                stack.append((fn, prefix + name + '.'))
258
    for pat in list(exclude) + ['ez_setup', 'distribute_setup']:
259
        out = [item for item in out if not fnmatchcase(item, pat)]
260
    return out
261
262
263
if __name__ == "__main__":
264
    # rewrite __init__.py in target_dir
265
    target_dir = abspath(sys.argv[1])
266
    version = get_version(join(target_dir, "__init__.py"))
267
    write_version_into_init(target_dir, version)
268