Completed
Pull Request — master (#867)
by
unknown
01:44
created

module_requirements()   B

Complexity

Conditions 5

Size

Total Lines 21

Duplication

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