Completed
Pull Request — master (#867)
by Joe
02:09
created

_with_bounds()   B

Complexity

Conditions 7

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 7
dl 0
loc 11
rs 7.3333
1
#!/usr/bin/env python
2
#
3
# Copyright 2014 Quantopian, Inc.
4
#
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
from __future__ import print_function
17
18
from distutils.version import StrictVersion
19
from itertools import starmap
20
from operator import lt, gt, eq, le, ge
21
from os.path import (
22
    abspath,
23
    dirname,
24
    join,
25
)
26
from pkg_resources import resource_filename
27
import re
28
from setuptools import (
29
    Extension,
30
    find_packages,
31
    setup,
32
)
33
import sys
34
35
import versioneer
36
37
38
class LazyCommandClass(dict):
39
    """
40
    Lazy command class that defers operations requiring Cython and numpy until
41
    they've actually been downloaded and installed by setup_requires.
42
    """
43
    def __contains__(self, key):
44
        return (
45
            key == 'build_ext'
46
            or super(LazyCommandClass, self).__contains__(key)
47
        )
48
49
    def __setitem__(self, key, value):
50
        if key == 'build_ext':
51
            raise AssertionError("build_ext overridden!")
52
        super(LazyCommandClass, self).__setitem__(key, value)
53
54
    def __getitem__(self, key):
55
        if key != 'build_ext':
56
            return super(LazyCommandClass, self).__getitem__(key)
57
58
        from Cython.Distutils import build_ext as cython_build_ext
59
60
        class build_ext(cython_build_ext):
61
            """
62
            Custom build_ext command that lazily adds numpy's include_dir to
63
            extensions.
64
            """
65
            def build_extensions(self):
66
                """
67
                Lazily append numpy's include directory to Extension includes.
68
69
                This is done here rather than at module scope because setup.py
70
                may be run before numpy has been installed, in which case
71
                importing numpy and calling `numpy.get_include()` will fail.
72
                """
73
                numpy_incl = resource_filename('numpy', 'core/include')
74
                for ext in self.extensions:
75
                    ext.include_dirs.append(numpy_incl)
76
77
                # This explicitly calls the superclass method rather than the
78
                # usual super() invocation because distutils' build_class, of
79
                # which Cython's build_ext is a subclass, is an old-style class
80
                # in Python 2, which doesn't support `super`.
81
                cython_build_ext.build_extensions(self)
82
        return build_ext
83
84
85
ext_modules = list(starmap(Extension, (
86
    ('zipline.assets._assets', ['zipline/assets/_assets.pyx']),
87
    ('zipline.lib.adjustment', ['zipline/lib/adjustment.pyx']),
88
    ('zipline.lib._float64window', ['zipline/lib/_float64window.pyx']),
89
    ('zipline.lib._int64window', ['zipline/lib/_int64window.pyx']),
90
    ('zipline.lib._uint8window', ['zipline/lib/_uint8window.pyx']),
91
    ('zipline.lib.rank', ['zipline/lib/rank.pyx']),
92
)))
93
94
STR_TO_CMP = {
95
    '<': lt,
96
    '<=': le,
97
    '=': eq,
98
    '==': eq,
99
    '>': gt,
100
    '>=': ge,
101
}
102
103
104
def _filter_requirements(lines_iter):
105
    for line in lines_iter:
106
        line = line.strip()
107
        if not line or line.startswith('#'):
108
            continue
109
110
        # pip install -r understands line with ;python_version<'3.0', but
111
        # whatever happens inside extras_requires doesn't.  Parse the line
112
        # manually and conditionally add it if needed.
113
        if ';' not in line:
114
            yield line
115
            continue
116
117
        requirement, version_spec = line.split(';')
118
        try:
119
            groups = re.match(
120
                "(python_version)([<>=]{1,2})(')([0-9\.]+)(')(.*)",
121
                version_spec,
122
            ).groups()
123
            comp = STR_TO_CMP[groups[1]]
124
            version_spec = StrictVersion(groups[3])
125
        except Exception as e:
126
            # My kingdom for a 'raise from'!
127
            raise AssertionError(
128
                "Couldn't parse requirement line; '%s'\n"
129
                "Error was:\n"
130
                "%r" % (line, e)
131
            )
132
133
        sys_version = '.'.join(list(map(str, sys.version_info[:3])))
134
        if comp(sys_version, version_spec):
135
            yield requirement
136
137
138
REQ_UPPER_BOUNDS = {
139
}
140
141
142
def _with_bounds(req):
143
    try:
144
        req, lower = req.split('==')
145
    except ValueError:
146
        return req
147
    else:
148
        with_bounds = [req, '>=', lower]
149
        upper = REQ_UPPER_BOUNDS.get(req)
150
        if upper:
151
            with_bounds.extend([',', upper])
152
        return ''.join(with_bounds)
153
154
155
def read_requirements(path, strict_bounds):
156
    """
157
    Read a requirements.txt file, expressed as a path relative to Zipline root.
158
159
    Returns requirements with the pinned versions as lower bounds
160
    if `strict_bounds` is falsey.
161
    """
162
    real_path = join(dirname(abspath(__file__)), path)
163
    with open(real_path) as f:
164
        reqs = _filter_requirements(f.readlines())
165
166
        if strict_bounds:
167
            return list(reqs)
168
        else:
169
            return list(map(_with_bounds, reqs))
170
171
172
def install_requires(strict_bounds=False):
173
    return read_requirements('etc/requirements.txt',
174
                             strict_bounds=strict_bounds)
175
176
177
def extras_requires():
178
    dev_reqs = read_requirements('etc/requirements_dev.txt',
179
                                 strict_bounds=True)
180
    talib_reqs = ['TA-Lib==0.4.9']
181
    return {
182
        'dev': dev_reqs,
183
        'talib': talib_reqs,
184
        'all': dev_reqs + talib_reqs,
185
    }
186
187
188
def module_requirements(requirements_path, module_names):
189
    module_names = set(module_names)
190
    found = set()
191
    module_lines = []
192
    parser = re.compile("([^=<>]+)([<=>]{1,2})(.*)")
193
    for line in read_requirements(requirements_path, strict_bounds=False):
194
        match = parser.match(line)
195
        if match is None:
196
            raise AssertionError("Could not parse requirement: '%s'" % line)
197
198
        groups = match.groups()
199
        name = groups[0]
200
        if name in module_names:
201
            found.add(name)
202
            module_lines.append(line)
203
204
    if found != module_names:
205
        raise AssertionError(
206
            "No requirements found for %s." % module_names - found
207
        )
208
    return module_lines
209
210
211
def setup_requires():
212
    if not set(sys.argv) & {'install', 'develop', 'egg_info', 'bdist_wheel'}:
213
        return []
214
215
    required = ('Cython', 'numpy')
216
    return module_requirements('etc/requirements.txt', required)
217
218
219
setup(
220
    name='zipline',
221
    version=versioneer.get_version(),
222
    cmdclass=LazyCommandClass(versioneer.get_cmdclass()),
223
    description='A backtester for financial algorithms.',
224
    author='Quantopian Inc.',
225
    author_email='[email protected]',
226
    packages=find_packages('.', include=['zipline', 'zipline.*']),
227
    ext_modules=ext_modules,
228
    scripts=['scripts/run_algo.py'],
229
    include_package_data=True,
230
    license='Apache 2.0',
231
    classifiers=[
232
        'Development Status :: 4 - Beta',
233
        'License :: OSI Approved :: Apache Software License',
234
        'Natural Language :: English',
235
        'Programming Language :: Python',
236
        'Programming Language :: Python :: 2.7',
237
        'Programming Language :: Python :: 3.3',
238
        'Programming Language :: Python :: 3.4',
239
        'Operating System :: OS Independent',
240
        'Intended Audience :: Science/Research',
241
        'Topic :: Office/Business :: Financial',
242
        'Topic :: Scientific/Engineering :: Information Analysis',
243
        'Topic :: System :: Distributed Computing',
244
    ],
245
    install_requires=install_requires(),
246
    setup_requires=setup_requires(),
247
    extras_require=extras_requires(),
248
    url="http://zipline.io",
249
)
250