Completed
Push — develop ( ce1198...1cbae8 )
by Kale
9s
created

write_version_into_init()   C

Complexity

Conditions 10

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
c 2
b 0
f 0
dl 0
loc 15
rs 6

How to fix   Complexity   

Complexity

Complex classes like write_version_into_init() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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