From e7269a880c752655c27f8fedb764c3852d9202d6 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Wed, 10 Feb 2016 18:34:59 -0500 Subject: [PATCH] BUG: Add right-binding operators to NumExprFilter. Fixes a bug where doing a boolean comparison between a non-numexpr Filter and a NumExprFilter would fail because we don't implement `__rand__` and `__ror__`. --- tests/pipeline/test_numerical_expression.py | 48 +++++++++++++++++---- zipline/pipeline/filters/filter.py | 7 +++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/tests/pipeline/test_numerical_expression.py b/tests/pipeline/test_numerical_expression.py index 8dcb7321..a49c166a 100644 --- a/tests/pipeline/test_numerical_expression.py +++ b/tests/pipeline/test_numerical_expression.py @@ -1,6 +1,5 @@ from operator import ( add, - and_, ge, gt, le, @@ -8,13 +7,13 @@ from operator import ( methodcaller, mul, ne, - or_, ) from unittest import TestCase import numpy from numpy import ( arange, + array, eye, float64, full, @@ -27,7 +26,7 @@ from pandas import ( Int64Index, ) -from zipline.pipeline import Factor +from zipline.pipeline import Factor, Filter from zipline.pipeline.expression import ( NumericalExpression, NUMEXPR_MATH_FUNCS, @@ -55,6 +54,11 @@ class H(Factor): window_length = 0 +class NonExprFilter(Filter): + inputs = () + window_length = 0 + + class DateFactor(Factor): dtype = datetime64ns_dtype inputs = () @@ -460,26 +464,52 @@ class NumericalExpressionTestCase(TestCase): def test_boolean_binops(self): f, g, h = self.f, self.g, self.h + + # Add a non-numexpr filter to ensure that we correctly handle + # delegation to NumericalExpression. + custom_filter = NonExprFilter() + custom_filter_mask = array( + [[0, 1, 0, 1, 0], + [0, 0, 1, 0, 0], + [1, 0, 0, 0, 0], + [0, 0, 1, 1, 0], + [0, 0, 0, 1, 0]], + dtype=bool, + ) + self.fake_raw_data = { f: arange(25).reshape(5, 5), g: arange(25).reshape(5, 5) - eye(5), h: full((5, 5), 5), + custom_filter: custom_filter_mask, } # Should be True on the diagonal. - eye_filter = f > g + eye_filter = (f > g) + # Should be True in the first row only. first_row_filter = f < h eye_mask = eye(5, dtype=bool) + first_row_mask = zeros((5, 5), dtype=bool) first_row_mask[0] = 1 self.check_output(eye_filter, eye_mask) self.check_output(first_row_filter, first_row_mask) - for op in (and_, or_): # NumExpr doesn't support xor. - self.check_output( - op(eye_filter, first_row_filter), - op(eye_mask, first_row_mask), - ) + def gen_boolops(a, b, c): + yield (a & b) & c + yield (a & b) | c + yield (a | b) & c + yield (a | b) | c + yield a & (b & c) + yield a & (b | c) + yield a | (b & c) + yield a | (b | c) + + exprs = gen_boolops(eye_filter, custom_filter, first_row_filter) + arrays = gen_boolops(eye_mask, custom_filter_mask, first_row_mask) + + for expr, expected in zip(exprs, arrays): + self.check_output(expr, expected) diff --git a/zipline/pipeline/filters/filter.py b/zipline/pipeline/filters/filter.py index fa450254..f3c02924 100644 --- a/zipline/pipeline/filters/filter.py +++ b/zipline/pipeline/filters/filter.py @@ -124,6 +124,13 @@ class Filter(CompositeTerm): for op in FILTER_BINOPS } ) + clsdict.update( + { + method_name_for_op(op, commute=True): binary_operator(op) + for op in FILTER_BINOPS + } + ) + __invert__ = unary_operator('~') def _validate(self):