From 381a231725337759d5a7753a00b9a13b626f0921 Mon Sep 17 00:00:00 2001 From: Scott Sanderson Date: Sat, 19 Mar 2016 16:41:02 -0400 Subject: [PATCH] MAINT: Clean up mixin usage. - Use RestrictedDTypeMixin for dtype validation in Filter/Factor/Classifier. - Use new LatestMixin for Latest{Filter,Factor,Classifier} instead of duplicating logic across all three. - Always ignore return values in _validate. - Consistently call super() first in validation mixins. --- zipline/pipeline/classifiers/__init__.py | 3 +- zipline/pipeline/classifiers/classifier.py | 35 +++++++++------- zipline/pipeline/classifiers/latest.py | 29 ------------- zipline/pipeline/expression.py | 2 +- zipline/pipeline/factors/__init__.py | 4 +- zipline/pipeline/factors/factor.py | 35 ++++++++-------- zipline/pipeline/factors/latest.py | 18 -------- zipline/pipeline/filters/__init__.py | 6 +-- zipline/pipeline/filters/filter.py | 14 +++++-- zipline/pipeline/filters/latest.py | 29 ------------- zipline/pipeline/mixins.py | 48 ++++++++++++++++++++-- zipline/pipeline/term.py | 10 ++--- 12 files changed, 106 insertions(+), 127 deletions(-) delete mode 100644 zipline/pipeline/classifiers/latest.py delete mode 100644 zipline/pipeline/factors/latest.py delete mode 100644 zipline/pipeline/filters/latest.py diff --git a/zipline/pipeline/classifiers/__init__.py b/zipline/pipeline/classifiers/__init__.py index a6b93d86..b512100f 100644 --- a/zipline/pipeline/classifiers/__init__.py +++ b/zipline/pipeline/classifiers/__init__.py @@ -1,5 +1,4 @@ -from .classifier import Classifier, CustomClassifier, Everything -from .latest import Latest +from .classifier import Classifier, CustomClassifier, Everything, Latest __all__ = [ 'Classifier', diff --git a/zipline/pipeline/classifiers/classifier.py b/zipline/pipeline/classifiers/classifier.py index 514dbf6d..84c9bb83 100644 --- a/zipline/pipeline/classifiers/classifier.py +++ b/zipline/pipeline/classifiers/classifier.py @@ -3,26 +3,19 @@ classifier.py """ from numpy import zeros, where -from zipline.errors import UnsupportedDataType from zipline.pipeline.term import ComputableTerm from zipline.utils.numpy_utils import int64_dtype -from ..mixins import CustomTermMixin, PositiveWindowLengthMixin +from ..mixins import ( + CustomTermMixin, + LatestMixin, + PositiveWindowLengthMixin, + RestrictedDTypeMixin +) -class Classifier(ComputableTerm): - - def _validate(self): - # Run superclass validation first so that we handle `dtype not passed` - # before this. - retval = super(Classifier, self)._validate() - # TODO: Support strings here. - if self.dtype != int64_dtype: - raise UnsupportedDataType( - typename=type(self).__name__, - dtype=self.dtype - ) - return retval +class Classifier(RestrictedDTypeMixin, ComputableTerm): + ALLOWED_DTYPES = (int64_dtype,) # Used by RestrictedDTypeMixin class Everything(Classifier): @@ -52,3 +45,15 @@ class CustomClassifier(PositiveWindowLengthMixin, CustomTermMixin, Classifier): zipline.pipeline.CustomFilter """ pass + + +class Latest(LatestMixin, CustomClassifier): + """ + A classifier producing the latest value of an input. + + See Also + -------- + zipline.pipeline.data.dataset.BoundColumn.latest + zipline.pipeline.factors.factor.Latest + zipline.pipeline.filters.filter.Latest + """ diff --git a/zipline/pipeline/classifiers/latest.py b/zipline/pipeline/classifiers/latest.py deleted file mode 100644 index efc1dd55..00000000 --- a/zipline/pipeline/classifiers/latest.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Classifier that produces the most most recently-known value of a -integer-valued column. -""" -from zipline.utils.numpy_utils import int64_dtype - -from .classifier import CustomClassifier -from ..mixins import SingleInputMixin - - -class Latest(SingleInputMixin, CustomClassifier): - """ - Filter producing the most recently-known value of `inputs[0]` on each day. - """ - window_length = 1 - - def compute(self, today, assets, out, data): - out[:] = data[-1] - - def _validate(self): - if self.inputs[0].dtype != int64_dtype: - raise TypeError( - "{name} expected an input of dtype int64, " - "but got {not_bool} instead.".format( - name=type(self).__name__, - not_bool=self.inputs[0].dtype, - ) - ) - super(Latest, self)._validate() diff --git a/zipline/pipeline/expression.py b/zipline/pipeline/expression.py index f61d67db..50d45a5e 100644 --- a/zipline/pipeline/expression.py +++ b/zipline/pipeline/expression.py @@ -223,7 +223,7 @@ class NumericalExpression(ComputableTerm): expected_indices, expr_indices, ) ) - return super(NumericalExpression, self)._validate() + super(NumericalExpression, self)._validate() def _compute(self, arrays, dates, assets, mask): """ diff --git a/zipline/pipeline/factors/__init__.py b/zipline/pipeline/factors/__init__.py index 4961fd31..d02902fd 100644 --- a/zipline/pipeline/factors/__init__.py +++ b/zipline/pipeline/factors/__init__.py @@ -1,8 +1,8 @@ from .factor import ( - Factor, CustomFactor, + Factor, + Latest ) -from .latest import Latest from .events import ( BusinessDaysSinceCashBuybackAuth, BusinessDaysUntilNextEarnings, diff --git a/zipline/pipeline/factors/factor.py b/zipline/pipeline/factors/factor.py index 106f24a8..c510878e 100644 --- a/zipline/pipeline/factors/factor.py +++ b/zipline/pipeline/factors/factor.py @@ -8,16 +8,15 @@ from numbers import Number from numpy import inf, where, nanstd from toolz import curry -from zipline.errors import ( - UnknownRankMethod, - UnsupportedDataType, -) +from zipline.errors import UnknownRankMethod from zipline.lib.normalize import naive_grouped_rowwise_apply from zipline.lib.rank import masked_rankdata_2d from zipline.pipeline.classifiers import Classifier, Everything from zipline.pipeline.mixins import ( CustomTermMixin, + LatestMixin, PositiveWindowLengthMixin, + RestrictedDTypeMixin, SingleInputMixin, ) from zipline.pipeline.term import ( @@ -396,10 +395,12 @@ float64_only = restrict_to_dtype( FACTOR_DTYPES = frozenset([datetime64ns_dtype, float64_dtype, int64_dtype]) -class Factor(ComputableTerm): +class Factor(RestrictedDTypeMixin, ComputableTerm): """ Pipeline API expression producing numerically-valued outputs. """ + ALLOWED_DTYPES = FACTOR_DTYPES # Used by RestrictedDTypeMixin + # Dynamically add functions for creating NumExprFactor/NumExprFilter # instances. clsdict = locals() @@ -436,17 +437,6 @@ class Factor(ComputableTerm): eq = binary_operator('==') - def _validate(self): - # Do superclass validation first so that `NotSpecified` dtypes get - # handled. - retval = super(Factor, self)._validate() - if self.dtype not in FACTOR_DTYPES: - raise UnsupportedDataType( - typename=type(self).__name__, - dtype=self.dtype - ) - return retval - @expect_types( mask=(Filter, NotSpecifiedType), groupby=(Classifier, NotSpecifiedType), @@ -1097,3 +1087,16 @@ class CustomFactor(PositiveWindowLengthMixin, CustomTermMixin, Factor): median_low15 = MedianValue([USEquityPricing.low], window_length=15) ''' dtype = float64_dtype + + +class Latest(LatestMixin, CustomFactor): + """ + Factor producing the most recently-known value of `inputs[0]` on each day. + + The `.latest` attribute of DataSet columns returns an instance of this + Factor. + """ + window_length = 1 + + def compute(self, today, assets, out, data): + out[:] = data[-1] diff --git a/zipline/pipeline/factors/latest.py b/zipline/pipeline/factors/latest.py deleted file mode 100644 index 4a7e3092..00000000 --- a/zipline/pipeline/factors/latest.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -Factor that produces the most most recently-known value of Column. -""" -from .factor import CustomFactor -from ..mixins import SingleInputMixin - - -class Latest(SingleInputMixin, CustomFactor): - """ - Factor producing the most recently-known value of `inputs[0]` on each day. - - The `.latest` attribute of DataSet columns returns an instance of this - Factor. - """ - window_length = 1 - - def compute(self, today, assets, out, data): - out[:] = data[-1] diff --git a/zipline/pipeline/filters/__init__.py b/zipline/pipeline/filters/__init__.py index fc88247a..4f05fc6f 100644 --- a/zipline/pipeline/filters/__init__.py +++ b/zipline/pipeline/filters/__init__.py @@ -1,17 +1,17 @@ from .filter import ( CustomFilter, Filter, - NumExprFilter, + Latest, NullFilter, + NumExprFilter, PercentileFilter, ) -from .latest import Latest __all__ = [ 'CustomFilter', 'Filter', 'Latest', - 'NumExprFilter', 'NullFilter', + 'NumExprFilter', 'PercentileFilter', ] diff --git a/zipline/pipeline/filters/filter.py b/zipline/pipeline/filters/filter.py index 5d72811f..9d412329 100644 --- a/zipline/pipeline/filters/filter.py +++ b/zipline/pipeline/filters/filter.py @@ -16,7 +16,9 @@ from zipline.errors import ( from zipline.lib.rank import ismissing from zipline.pipeline.mixins import ( CustomTermMixin, + LatestMixin, PositiveWindowLengthMixin, + RestrictedDTypeMixin, SingleInputMixin, ) from zipline.pipeline.term import ComputableTerm, Term @@ -26,7 +28,6 @@ from zipline.pipeline.expression import ( method_name_for_op, NumericalExpression, ) -from zipline.utils.control_flow import nullctx from zipline.utils.numpy_utils import bool_dtype @@ -114,10 +115,11 @@ def unary_operator(op): return unary_operator -class Filter(ComputableTerm): +class Filter(RestrictedDTypeMixin, ComputableTerm): """ Pipeline API expression producing boolean-valued outputs. """ + ALLOWED_DTYPES = (bool_dtype,) # Used by RestrictedDTypeMixin dtype = bool_dtype clsdict = locals() @@ -324,4 +326,10 @@ class CustomFilter(PositiveWindowLengthMixin, CustomTermMixin, Filter): -------- zipline.pipeline.factors.factor.CustomFactor """ - ctx = nullctx() + + +class Latest(LatestMixin, CustomFilter): + """ + Filter producing the most recently-known value of `inputs[0]` on each day. + """ + pass diff --git a/zipline/pipeline/filters/latest.py b/zipline/pipeline/filters/latest.py deleted file mode 100644 index f9588804..00000000 --- a/zipline/pipeline/filters/latest.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Filter that produces the most most recently-known value of a boolean-valued -Column. -""" -from zipline.utils.numpy_utils import bool_dtype - -from .filter import CustomFilter -from ..mixins import SingleInputMixin - - -class Latest(SingleInputMixin, CustomFilter): - """ - Filter producing the most recently-known value of `inputs[0]` on each day. - """ - window_length = 1 - - def compute(self, today, assets, out, data): - out[:] = data[-1] - - def _validate(self): - if self.inputs[0].dtype != bool_dtype: - raise TypeError( - "{name} expected an input of dtype bool, " - "but got {not_bool} instead.".format( - name=type(self).__name__, - not_bool=self.inputs[0].dtype, - ) - ) - super(Latest, self)._validate() diff --git a/zipline/pipeline/mixins.py b/zipline/pipeline/mixins.py index 11e5cbcf..4e8a27b5 100644 --- a/zipline/pipeline/mixins.py +++ b/zipline/pipeline/mixins.py @@ -4,7 +4,7 @@ Mixins classes for use with Filters and Factors. from numpy import full_like from zipline.utils.control_flow import nullctx -from zipline.errors import WindowLengthNotPositive +from zipline.errors import WindowLengthNotPositive, UnsupportedDataType from .term import NotSpecified @@ -14,9 +14,9 @@ class PositiveWindowLengthMixin(object): Validation mixin enforcing that a Term gets a positive WindowLength """ def _validate(self): + super(PositiveWindowLengthMixin, self)._validate() if not self.windowed: raise WindowLengthNotPositive(window_length=self.window_length) - return super(PositiveWindowLengthMixin, self)._validate() class SingleInputMixin(object): @@ -24,6 +24,7 @@ class SingleInputMixin(object): Validation mixin enforcing that a Term gets a length-1 inputs list. """ def _validate(self): + super(SingleInputMixin, self)._validate() num_inputs = len(self.inputs) if num_inputs != 1: raise ValueError( @@ -33,7 +34,26 @@ class SingleInputMixin(object): num_inputs=num_inputs ) ) - return super(SingleInputMixin, self)._validate() + + +class RestrictedDTypeMixin(object): + """ + Validation mixin enforcing that a term has a specific dtype. + """ + ALLOWED_DTYPES = NotSpecified + + def _validate(self): + super(RestrictedDTypeMixin, self)._validate() + assert self.ALLOWED_DTYPES is not NotSpecified, ( + "ALLOWED_DTYPES not supplied on subclass " + "of RestrictedDTypeMixin: %s." % type(self).__name__ + ) + + if self.dtype not in self.ALLOWED_DTYPES: + raise UnsupportedDataType( + typename=type(self.__name__), + dtype=self.dtype, + ) class CustomTermMixin(object): @@ -105,3 +125,25 @@ class CustomTermMixin(object): def short_repr(self): return type(self).__name__ + '(%d)' % self.window_length + + +class LatestMixin(SingleInputMixin): + """ + Mixin for behavior shared by Custom{Factor,Filter,Classifier}. + """ + window_length = 1 + + def compute(self, today, assets, out, data): + out[:] = data[-1] + + def _validate(self): + super(LatestMixin, self)._validate() + if self.inputs[0].dtype != self.dtype: + raise TypeError( + "{name} expected an input of dtype {expected}, " + "but got {actual} instead.".format( + name=type(self).__name__, + expected=self.dtype, + actual=self.inputs[0].dtype, + ) + ) diff --git a/zipline/pipeline/term.py b/zipline/pipeline/term.py index b424bafc..07b0ef8f 100644 --- a/zipline/pipeline/term.py +++ b/zipline/pipeline/term.py @@ -398,14 +398,14 @@ class ComputableTerm(Term): ) def _validate(self): - """ - Assert that this term is well-formed. This should be called exactly - once, at the end of Term._init(). - """ + super(ComputableTerm, self)._validate() + if self.inputs is NotSpecified: raise TermInputsNotSpecified(termname=type(self).__name__) + if self.window_length is NotSpecified: raise WindowLengthNotSpecified(termname=type(self).__name__) + if self.mask is NotSpecified: # This isn't user error, this is a bug in our code. raise AssertionError("{term} has no mask".format(term=self)) @@ -415,8 +415,6 @@ class ComputableTerm(Term): if child.windowed: raise WindowedInputToWindowedTerm(parent=self, child=child) - return super(ComputableTerm, self)._validate() - def _compute(self, inputs, dates, assets, mask): """ Subclasses should implement this to perform actual computation.