Completed
Pull Request — master (#948)
by
unknown
01:23
created

zipline.utils.coerce()   B

Complexity

Conditions 3

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 35
rs 8.8571

1 Method

Rating   Name   Duplication   Size   Complexity  
A zipline.utils.preprocessor() 0 4 2
1
# Copyright 2015 Quantopian, Inc.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
6
#
7
#     http://www.apache.org/licenses/LICENSE-2.0
8
#
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
from functools import partial
15
from operator import attrgetter
16
17
from numpy import dtype
18
from six import iteritems, string_types, PY3
19
from toolz import valmap, complement, compose
20
import toolz.curried.operator as op
21
22
from zipline.utils.preprocess import preprocess
23
24
25
def ensure_upper_case(func, argname, arg):
26
    if isinstance(arg, string_types):
27
        return arg.upper()
28
    else:
29
        raise TypeError(
30
            "{0}() expected argument '{1}' to"
31
            " be a string, but got {2} instead.".format(
32
                func.__name__, argname, arg,)
33
        )
34
35
36
def ensure_dtype(func, argname, arg):
37
    """
38
    Argument preprocessor that converts the input into a numpy dtype.
39
40
    Usage
41
    -----
42
    >>> import numpy as np
43
    >>> from zipline.utils.preprocess import preprocess
44
    >>> @preprocess(dtype=ensure_dtype)
45
    ... def foo(dtype):
46
    ...     return dtype
47
    ...
48
    >>> foo(float)
49
    dtype('float64')
50
    """
51
    try:
52
        return dtype(arg)
53
    except TypeError:
54
        raise TypeError(
55
            "{func}() couldn't convert argument "
56
            "{argname}={arg!r} to a numpy dtype.".format(
57
                func=_qualified_name(func),
58
                argname=argname,
59
                arg=arg,
60
            ),
61
        )
62
63
64
def expect_dtypes(*_pos, **named):
65
    """
66
    Preprocessing decorator that verifies inputs have expected numpy dtypes.
67
68
    Usage
69
    -----
70
    >>> from numpy import dtype, arange
71
    >>> @expect_dtypes(x=dtype(int))
72
    ... def foo(x, y):
73
    ...    return x, y
74
    ...
75
    >>> foo(arange(3), 'foo')
76
    (array([0, 1, 2]), 'foo')
77
    >>> foo(arange(3, dtype=float), 'foo')
78
    Traceback (most recent call last):
79
       ...
80
    TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead.  # noqa
81
    """
82
    if _pos:
83
        raise TypeError("expect_dtypes() only takes keyword arguments.")
84
85
    for name, type_ in iteritems(named):
86
        if not isinstance(type_, (dtype, tuple)):
87
            raise TypeError(
88
                "expect_dtypes() expected a numpy dtype or tuple of dtypes"
89
                " for argument {name!r}, but got {dtype} instead.".format(
90
                    name=name, dtype=dtype,
91
                )
92
            )
93
    return preprocess(**valmap(_expect_dtype, named))
94
95
96
def _expect_dtype(_dtype_or_dtype_tuple):
97
    """
98
    Factory for dtype-checking functions that work the @preprocess decorator.
99
    """
100
    # Slightly different messages for dtype and tuple of dtypes.
101
    if isinstance(_dtype_or_dtype_tuple, tuple):
102
        allowed_dtypes = _dtype_or_dtype_tuple
103
    else:
104
        allowed_dtypes = (_dtype_or_dtype_tuple,)
105
    template = (
106
        "%(funcname)s() expected a value with dtype {dtype_str} "
107
        "for argument '%(argname)s', but got %(actual)r instead."
108
    ).format(dtype_str=' or '.join(repr(d.name) for d in allowed_dtypes))
109
110
    def check_dtype(value):
111
        return getattr(value, 'dtype', None) not in allowed_dtypes
112
113
    def display_bad_value(value):
114
        # If the bad value has a dtype, but it's wrong, show the dtype name.
115
        try:
116
            return value.dtype.name
117
        except AttributeError:
118
            return value
119
120
    return make_check(
121
        exc_type=TypeError,
122
        template=template,
123
        pred=check_dtype,
124
        actual=display_bad_value,
125
    )
126
127
128
def expect_types(*_pos, **named):
129
    """
130
    Preprocessing decorator that verifies inputs have expected types.
131
132
    Usage
133
    -----
134
    >>> @expect_types(x=int, y=str)
135
    ... def foo(x, y):
136
    ...    return x, y
137
    ...
138
    >>> foo(2, '3')
139
    (2, '3')
140
    >>> foo(2.0, '3')
141
    Traceback (most recent call last):
142
       ...
143
    TypeError: foo() expected an argument of type 'int' for argument 'x', but got float instead.  # noqa
144
    """
145
    if _pos:
146
        raise TypeError("expect_types() only takes keyword arguments.")
147
148
    for name, type_ in iteritems(named):
149
        if not isinstance(type_, (type, tuple)):
150
            raise TypeError(
151
                "expect_types() expected a type or tuple of types for "
152
                "argument '{name}', but got {type_} instead.".format(
153
                    name=name, type_=type_,
154
                )
155
            )
156
157
    return preprocess(**valmap(_expect_type, named))
158
159
160
if PY3:
161
    _qualified_name = attrgetter('__qualname__')
162
else:
163
    def _qualified_name(obj):
164
        """
165
        Return the fully-qualified name (ignoring inner classes) of a type.
166
        """
167
        module = obj.__module__
168
        if module in ('__builtin__', '__main__', 'builtins'):
169
            return obj.__name__
170
        return '.'.join([module, obj.__name__])
171
172
173
def make_check(exc_type, template, pred, actual):
174
    """
175
    Factory for making preprocessing functions that check a predicate on the
176
    input value.
177
178
    Parameters
179
    ----------
180
    exc_type : Exception
181
        The exception type to raise if the predicate fails.
182
    template : str
183
        A template string to use to create error messages.
184
        Should have %-style named template parameters for 'funcname',
185
        'argname', and 'actual'.
186
    pred : function[object -> bool]
187
        A function to call on the argument being preprocessed.  If the
188
        predicate returns `True`, we raise an instance of `exc_type`.
189
    actual : function[object -> object]
190
        A function to call on bad values to produce the value to display in the
191
        error message.
192
    """
193
194
    def _check(func, argname, argvalue):
195
        if pred(argvalue):
196
            raise exc_type(
197
                template % {
198
                    'funcname': _qualified_name(func),
199
                    'argname': argname,
200
                    'actual': actual(argvalue),
201
                },
202
            )
203
        return argvalue
204
    return _check
205
206
207
def _expect_type(type_):
208
    """
209
    Factory for type-checking functions that work the @preprocess decorator.
210
    """
211
    # Slightly different messages for type and tuple of types.
212
    _template = (
213
        "%(funcname)s() expected a value of type {type_or_types} "
214
        "for argument '%(argname)s', but got %(actual)s instead."
215
    )
216
    if isinstance(type_, tuple):
217
        template = _template.format(
218
            type_or_types=' or '.join(map(_qualified_name, type_))
219
        )
220
    else:
221
        template = _template.format(type_or_types=_qualified_name(type_))
222
223
    return make_check(
224
        TypeError,
225
        template,
226
        lambda v: not isinstance(v, type_),
227
        compose(_qualified_name, type),
228
    )
229
230
231
def optional(type_):
232
    """
233
    Helper for use with `expect_types` when an input can be `type_` or `None`.
234
235
    Returns an object such that both `None` and instances of `type_` pass
236
    checks of the form `isinstance(obj, optional(type_))`.
237
238
    Parameters
239
    ----------
240
    type_ : type
241
       Type for which to produce an option.
242
243
    Examples
244
    --------
245
    >>> isinstance({}, optional(dict))
246
    True
247
    >>> isinstance(None, optional(dict))
248
    True
249
    >>> isinstance(1, optional(dict))
250
    False
251
    """
252
    return (type_, type(None))
253
254
255
def expect_element(*_pos, **named):
256
    """
257
    Preprocessing decorator that verifies inputs are elements of some
258
    expected collection.
259
260
    Usage
261
    -----
262
    >>> @expect_element(x=('a', 'b'))
263
    ... def foo(x):
264
    ...    return x.upper()
265
    ...
266
    >>> foo('a')
267
    'A'
268
    >>> foo('b')
269
    'B'
270
    >>> foo('c')
271
    Traceback (most recent call last):
272
       ...
273
    ValueError: foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead.  # noqa
274
275
    Notes
276
    -----
277
    This uses the `in` operator (__contains__) to make the containment check.
278
    This allows us to use any custom container as long as the object supports
279
    the container protocol.
280
    """
281
    if _pos:
282
        raise TypeError("expect_element() only takes keyword arguments.")
283
284
    return preprocess(**valmap(_expect_element, named))
285
286
287
def coerce(from_, to, **to_kwargs):
288
    """
289
    A preprocessing decorator that coerces inputs of a given type by passing
290
    them to a callable.
291
292
    Parameters
293
    ----------
294
    from : type or tuple or types
295
        Inputs types on which to call ``to``.
296
    to : function
297
        Coercion function to call on inputs.
298
    **to_kwargs
299
        Additional keywords to forward to every call to ``to``.
300
301
    Usage
302
    -----
303
    >>> @preprocess(x=coerce(float, int), y=coerce(float, int))
304
    ... def floordiff(x, y):
305
    ...     return x - y
306
    ...
307
    >>> floordiff(3.2, 2.5)
308
    1
309
310
    >>> @preprocess(x=coerce(str, int, base=2), y=coerce(str, int, base=2))
311
    ... def add_binary_strings(x, y):
312
    ...     return bin(x + y)[2:]
313
    ...
314
    >>> add_binary_strings('101', '001')
315
    '110'
316
    """
317
    def preprocessor(func, argname, arg):
318
        if isinstance(arg, from_):
319
            return to(arg, **to_kwargs)
320
        return arg
321
    return preprocessor
322
323
324
coerce_string = partial(coerce, string_types)
325
326
327
def _expect_element(collection):
328
    template = (
329
        "%(funcname)s() expected a value in {collection} "
330
        "for argument '%(argname)s', but got %(actual)s instead."
331
    ).format(collection=collection)
332
    return make_check(
333
        ValueError,
334
        template,
335
        complement(op.contains(collection)),
336
        repr,
337
    )
338