RequestConstraint.evaluate()   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 35
Code Lines 31

Duplication

Lines 35
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 31
dl 35
loc 35
rs 2.9998
c 0
b 0
f 0
cc 15
nop 2

How to fix   Complexity   

Complexity

Complex classes like dynamicserialize.dstypes.com.raytheon.uf.common.dataquery.requests.RequestConstraint.RequestConstraint.evaluate() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#
2
#      SOFTWARE HISTORY
3
#
4
#     Date            Ticket#       Engineer       Description
5
#     ------------    ----------    -----------    --------------------------
6
#     Jun 01, 2016    5574          tgurney        Initial creation
7
#     Jun 27, 2016    5725          tgurney        Add NOT IN
8
#     Jul 22, 2016    2416          tgurney        Add evaluate()
9
#     Oct 05, 2018                  mjames@ucar    Python 3 types
10
#
11
#
12
13
import re
14
import six
15
from dynamicserialize.dstypes.com.raytheon.uf.common.time import DataTime
16
17
18 View Code Duplication
class RequestConstraint(object):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
19
20
    TOLERANCE = 0.0001
21
22
    IN_PATTERN = re.compile(',\s?')
23
24
    def __init__(self):
25
        self.constraintValue = None
26
        self.constraintType = None
27
28
    def getConstraintValue(self):
29
        return self.constraintValue
30
31
    def setConstraintValue(self, constraintValue):
32
        if hasattr(self, '_evalValue'):
33
            del self._evalValue
34
        self.constraintValue = constraintValue
35
36
    def getConstraintType(self):
37
        return self.constraintType
38
39
    def setConstraintType(self, constraintType):
40
        if hasattr(self, '_evalValue'):
41
            del self._evalValue
42
        self.constraintType = constraintType
43
44
    def evaluate(self, value):
45
        if not hasattr(self, '_evalValue'):
46
            self._setupEvalValue()
47
48
        if self.constraintType == 'EQUALS':
49
            return self._evalEquals(value)
50
        elif self.constraintType == 'NOT_EQUALS':
51
            return not self._evalEquals(value)
52
        elif self.constraintType == 'GREATER_THAN':
53
            return self._evalGreaterThan(value)
54
        elif self.constraintType == 'GREATER_THAN_EQUALS':
55
            return self._evalGreaterThanEquals(value)
56
        elif self.constraintType == 'LESS_THAN':
57
            return self._evalLessThan(value)
58
        elif self.constraintType == 'LESS_THAN_EQUALS':
59
            return self._evalLessThanEquals(value)
60
        elif self.constraintType == 'BETWEEN':
61
            return self._evalBetween(value)
62
        elif self.constraintType == 'IN':
63
            return self._evalIn(value)
64
        elif self.constraintType == 'NOT_IN':
65
            return not self._evalIn(value)
66
        elif self.constraintType == 'LIKE':
67
            return self._evalLike(value)
68
        # setupConstraintType already adds correct flags for ilike
69
        # on regex pattern
70
        elif self.constraintType == 'ILIKE':
71
            return self._evalLike(value)
72
        elif self.constraintType == 'ISNULL':
73
            return self._evalIsNull(value)
74
        elif self.constraintType == 'ISNOTNULL':
75
            return not self._evalIsNull(value)
76
        else:
77
            errmsg = '{} is not a valid constraint type.'
78
            raise ValueError(errmsg.format(self.constraintType))
79
80
    def _makeRegex(self, pattern, flags):
81
        """Make a pattern using % wildcard into a regex"""
82
        pattern = re.escape(pattern)
83
        pattern = pattern.replace('\\%', '.*')
84
        pattern = pattern.replace('\\_', '.')
85
        pattern = pattern + '$'
86
        return re.compile(pattern, flags)
87
88
    def _setupEvalValue(self):
89
        if self.constraintType == 'BETWEEN':
90
            self._evalValue = self.constraintValue.split('--')
91
            self._evalValue[0] = self._adjustValueType(self._evalValue[0])
92
            self._evalValue[1] = self._adjustValueType(self._evalValue[1])
93
        elif self.constraintType in ('IN', 'NOT_IN'):
94
            splitValue = self.IN_PATTERN.split(self.constraintValue)
95
            self._evalValue = {
96
                self._adjustValueType(value)
97
                for value in splitValue
98
                }
99
            # if collection now contains multiple types we have to force
100
            # everything to string instead
101
            initialType = next(iter(self._evalValue)).__class__
102
            for item in self._evalValue:
103
                if item.__class__ is not initialType:
104
                    self._evalValue = {str(value) for value in splitValue}
105
                    break
106
        elif self.constraintType == 'LIKE':
107
            self._evalValue = self._makeRegex(self.constraintValue, re.DOTALL)
108
        elif self.constraintType == 'ILIKE':
109
            self._evalValue = self._makeRegex(self.constraintValue, re.IGNORECASE | re.DOTALL)
110
        elif self.constraintValue is None:
111
            self._evalValue = None
112
        else:
113
            self._evalValue = self._adjustValueType(self.constraintValue)
114
115
    def _adjustValueType(self, value):
116
        """
117
        Try to take part of a constraint value, encoded as a string, and
118
        return it as its 'true type'.
119
120
        _adjustValueType('3.0') -> 3.0
121
        _adjustValueType('3') -> 3.0
122
        _adjustValueType('a string') -> 'a string'
123
        """
124
        try:
125
            return float(value)
126
        except ValueError:
127
            pass
128
        try:
129
            return DataTime(value)
130
        except ValueError:
131
            pass
132
        return value
133
134
    def _matchType(self, value, otherValue):
135
        """
136
        Return value coerced to be the same type as otherValue. If this is
137
        not possible, just return value unmodified.
138
        """
139
        if not isinstance(value, otherValue.__class__):
140
            try:
141
                return otherValue.__class__(value)
142
            except ValueError:
143
                pass
144
        return value
145
146
    def _evalEquals(self, value):
147
        value = self._matchType(value, self._evalValue)
148
        if isinstance(value, float):
149
            return abs(float(self._evalValue) - value) < self.TOLERANCE
150
        return value == self._evalValue
151
152
    def _evalGreaterThan(self, value):
153
        value = self._matchType(value, self._evalValue)
154
        return value > self._evalValue
155
156
    def _evalGreaterThanEquals(self, value):
157
        value = self._matchType(value, self._evalValue)
158
        return value >= self._evalValue
159
160
    def _evalLessThan(self, value):
161
        value = self._matchType(value, self._evalValue)
162
        return value < self._evalValue
163
164
    def _evalLessThanEquals(self, value):
165
        value = self._matchType(value, self._evalValue)
166
        return value <= self._evalValue
167
168
    def _evalBetween(self, value):
169
        value = self._matchType(value, self._evalValue[0])
170
        return self._evalValue[0] <= value <= self._evalValue[1]
171
172
    def _evalIn(self, value):
173
        anEvalValue = next(iter(self._evalValue))
174
        if isinstance(anEvalValue, float):
175
            for otherValue in self._evalValue:
176
                try:
177
                    if abs(otherValue - float(value)) < self.TOLERANCE:
178
                        return True
179
                except ValueError:
180
                    pass
181
            return False
182
        value = self._matchType(value, anEvalValue)
183
        return value in self._evalValue
184
185
    def _evalLike(self, value):
186
        value = self._matchType(value, self._evalValue)
187
        if self.constraintValue == '%':
188
            return True
189
        return self._evalValue.match(value) is not None
190
191
    def _evalIsNull(self, value):
192
        return value is None or value == 'null'
193
194
    # DAF-specific stuff begins here ##########################################
195
196
    CONSTRAINT_MAP = {'=': 'EQUALS',
197
                      '!=': 'NOT_EQUALS',
198
                      '>': 'GREATER_THAN',
199
                      '>=': 'GREATER_THAN_EQUALS',
200
                      '<': 'LESS_THAN',
201
                      '<=': 'LESS_THAN_EQUALS',
202
                      'IN': 'IN',
203
                      'NOT IN': 'NOT_IN'
204
                      }
205
206
    @staticmethod
207
    def _stringify(value):
208
        if six.PY2:
209
            if isinstance(value, (str, int, long, bool, float, unicode)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable long does not seem to be defined.
Loading history...
210
                return str(value)
211
            else:
212
                # Collections are not allowed; they are handled separately.
213
                # Arbitrary objects are not allowed because the string
214
                # representation may not be sufficient to reconstruct the object.
215
                raise TypeError('Constraint values of type ' + repr(type(value)) +
216
                                'are not allowed')
217
        else:
218
            if isinstance(value, (str, int, bool, float)):
219
                return str(value)
220
            else:
221
                # Collections are not allowed; they are handled separately.
222
                # Arbitrary objects are not allowed because the string
223
                # representation may not be sufficient to reconstruct the object.
224
                raise TypeError('Constraint values of type ' + repr(type(value)) +
225
                                'are not allowed')
226
227
    @classmethod
228
    def _constructIn(cls, constraintType, constraintValue):
229
        """Build a new "IN" or "NOT IN" constraint from an iterable."""
230
        try:
231
            iterator = iter(constraintValue)
232
        except TypeError:
233
            raise TypeError("value for IN / NOT IN constraint must be an iterable")
234
        stringValue = ', '.join(cls._stringify(item) for item in iterator)
235
        if not stringValue:
236
            raise ValueError('cannot use IN / NOT IN with empty collection')
237
        obj = cls()
238
        obj.setConstraintType(constraintType)
239
        obj.setConstraintValue(stringValue)
240
        return obj
241
242
    @classmethod
243
    def _constructEq(cls, constraintType, constraintValue):
244
        """Build a new = or != constraint. Handle None specially by making an
245
        "is null" or "is not null" instead.
246
        """
247
        obj = cls()
248
        if constraintValue is None:
249
            if constraintType == 'EQUALS':
250
                obj.setConstraintType('ISNULL')
251
            elif constraintType == 'NOT_EQUALS':
252
                obj.setConstraintType('ISNOTNULL')
253
        else:
254
            obj = cls._construct(constraintType, constraintValue)
255
        return obj
256
257
    @classmethod
258
    def _construct(cls, constraintType, constraintValue):
259
        """Build a new constraint."""
260
        stringValue = cls._stringify(constraintValue)
261
        obj = cls()
262
        obj.setConstraintType(constraintType)
263
        obj.setConstraintValue(stringValue)
264
        return obj
265
266
    @classmethod
267
    def new(cls, operator, constraintValue):
268
        """Build a new RequestConstraint."""
269
        try:
270
            constraintType = cls.CONSTRAINT_MAP[operator.upper()]
271
        except KeyError:
272
            errmsg = '{} is not a valid operator. Valid operators are: {}'
273
            validOperators = list(sorted(cls.CONSTRAINT_MAP.keys()))
274
            raise ValueError(errmsg.format(operator, validOperators))
275
        if constraintType in ('IN', 'NOT_IN'):
276
            return cls._constructIn(constraintType, constraintValue)
277
        elif constraintType in {'EQUALS', 'NOT_EQUALS'}:
278
            return cls._constructEq(constraintType, constraintValue)
279
        return cls._construct(constraintType, constraintValue)
280