Skip to content

Commit 8ed4d4e

Browse files
authored
fix: Supporting Dict Subclasses in Evaluation - Merge pull request #99 from growthbook/feat/isInstanceTypeCheck
Supporting Dict Subclasses in Evaluation
2 parents 604d742 + f08185c commit 8ed4d4e

2 files changed

Lines changed: 63 additions & 29 deletions

File tree

growthbook/core.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,47 +50,48 @@ def isOperatorObject(obj: Any) -> bool:
5050
return False
5151
return True
5252

53-
def getType(attributeValue) -> str:
54-
t = type(attributeValue)
53+
def _is_numeric(v: Any) -> bool:
54+
return isinstance(v, (int, float)) and not isinstance(v, bool)
5555

56+
def getType(attributeValue) -> str:
5657
if attributeValue is None:
5758
return "null"
58-
if t is int or t is float:
59+
if isinstance(attributeValue, bool):
60+
return "boolean"
61+
if _is_numeric(attributeValue):
5962
return "number"
60-
if t is str:
63+
if isinstance(attributeValue, str):
6164
return "string"
62-
if t is list or t is set:
65+
if isinstance(attributeValue, (list, set)):
6366
return "array"
64-
if t is dict:
67+
if isinstance(attributeValue, dict):
6568
return "object"
66-
if t is bool:
67-
return "boolean"
6869
return "unknown"
6970

7071
def getPath(attributes, path):
7172
current = attributes
7273
for segment in path.split("."):
73-
if type(current) is dict and segment in current:
74+
if isinstance(current, dict) and segment in current:
7475
current = current[segment]
7576
else:
7677
return None
7778
return current
7879

7980
def evalConditionValue(conditionValue, attributeValue, savedGroups, insensitive: bool = False) -> bool:
80-
if type(conditionValue) is dict and isOperatorObject(conditionValue):
81+
if isinstance(conditionValue, dict) and isOperatorObject(conditionValue):
8182
for key, value in conditionValue.items():
8283
if not evalOperatorCondition(key, attributeValue, value, savedGroups):
8384
return False
8485
return True
8586

8687
# Simple equality comparison with optional case-insensitivity
87-
if insensitive and type(conditionValue) is str and type(attributeValue) is str:
88+
if insensitive and isinstance(conditionValue, str) and isinstance(attributeValue, str):
8889
return conditionValue.lower() == attributeValue.lower()
8990

9091
return bool(conditionValue == attributeValue)
9192

9293
def elemMatch(condition, attributeValue, savedGroups) -> bool:
93-
if not type(attributeValue) is list:
94+
if not isinstance(attributeValue, list):
9495
return False
9596

9697
for item in attributeValue:
@@ -104,13 +105,13 @@ def elemMatch(condition, attributeValue, savedGroups) -> bool:
104105
return False
105106

106107
def compare(val1, val2) -> int:
107-
if (type(val1) is int or type(val1) is float) and not (type(val2) is int or type(val2) is float):
108+
if _is_numeric(val1) and not _is_numeric(val2):
108109
if (val2 is None):
109110
val2 = 0
110111
else:
111112
val2 = float(val2)
112113

113-
if (type(val2) is int or type(val2) is float) and not (type(val1) is int or type(val1) is float):
114+
if _is_numeric(val2) and not _is_numeric(val1):
114115
if (val1 is None):
115116
val1 = 0
116117
else:
@@ -166,13 +167,13 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
166167
elif operator == "$vgte":
167168
return paddedVersionString(attributeValue) >= paddedVersionString(conditionValue)
168169
elif operator == "$inGroup":
169-
if not type(conditionValue) is str:
170+
if not isinstance(conditionValue, str):
170171
return False
171172
if not conditionValue in savedGroups:
172173
return False
173174
return isIn(savedGroups[conditionValue] or [], attributeValue)
174175
elif operator == "$notInGroup":
175-
if not type(conditionValue) is str:
176+
if not isinstance(conditionValue, str):
176177
return False
177178
if not conditionValue in savedGroups:
178179
return True
@@ -202,33 +203,33 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
202203
except Exception:
203204
return False
204205
elif operator == "$in":
205-
if not type(conditionValue) is list:
206+
if not isinstance(conditionValue, list):
206207
return False
207208
return isIn(conditionValue, attributeValue)
208209
elif operator == "$nin":
209-
if not type(conditionValue) is list:
210+
if not isinstance(conditionValue, list):
210211
return False
211212
return not isIn(conditionValue, attributeValue)
212213
elif operator == "$ini":
213-
if not type(conditionValue) is list:
214+
if not isinstance(conditionValue, list):
214215
return False
215216
return isIn(conditionValue, attributeValue, insensitive=True)
216217
elif operator == "$nini":
217-
if not type(conditionValue) is list:
218+
if not isinstance(conditionValue, list):
218219
return False
219220
return not isIn(conditionValue, attributeValue, insensitive=True)
220221
elif operator == "$elemMatch":
221222
return elemMatch(conditionValue, attributeValue, savedGroups)
222223
elif operator == "$size":
223-
if not (type(attributeValue) is list):
224+
if not isinstance(attributeValue, list):
224225
return False
225226
return evalConditionValue(conditionValue, len(attributeValue), savedGroups)
226227
elif operator == "$all":
227-
if not type(conditionValue) is list:
228+
if not isinstance(conditionValue, list):
228229
return False
229230
return isInAll(conditionValue, attributeValue, savedGroups, insensitive=False)
230231
elif operator == "$alli":
231-
if not type(conditionValue) is list:
232+
if not isinstance(conditionValue, list):
232233
return False
233234
return isInAll(conditionValue, attributeValue, savedGroups, insensitive=True)
234235
elif operator == "$exists":
@@ -243,10 +244,10 @@ def evalOperatorCondition(operator, attributeValue, conditionValue, savedGroups)
243244

244245
def paddedVersionString(input) -> str:
245246
# If input is a number, convert to a string
246-
if type(input) is int or type(input) is float:
247+
if _is_numeric(input):
247248
input = str(input)
248249

249-
if not input or type(input) is not str:
250+
if not input or not isinstance(input, str):
250251
input = "0"
251252

252253
# Remove build info and leading `v` if any
@@ -268,10 +269,10 @@ def isIn(conditionValue, attributeValue, insensitive: bool = False) -> bool:
268269
if insensitive:
269270
# Helper function to case-fold values (lowercase for strings)
270271
def case_fold(val):
271-
return val.lower() if type(val) is str else val
272+
return val.lower() if isinstance(val, str) else val
272273

273274
# Do an intersection if attribute is an array (insensitive)
274-
if type(attributeValue) is list:
275+
if isinstance(attributeValue, list):
275276
return any(
276277
case_fold(el) == case_fold(exp)
277278
for el in attributeValue
@@ -280,13 +281,13 @@ def case_fold(val):
280281
return any(case_fold(attributeValue) == case_fold(exp) for exp in conditionValue)
281282

282283
# Case-sensitive behavior (original)
283-
if type(attributeValue) is list:
284+
if isinstance(attributeValue, list):
284285
return bool(set(conditionValue) & set(attributeValue))
285286
return attributeValue in conditionValue
286287

287288
def isInAll(conditionValue, attributeValue, savedGroups, insensitive: bool = False) -> bool:
288289
"""Check if attributeValue (array) contains all elements in conditionValue"""
289-
if not type(attributeValue) is list:
290+
if not isinstance(attributeValue, list):
290291
return False
291292

292293
for cond in conditionValue:

tests/test_dict_subclass.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
from growthbook.core import getPath, evalCondition
3+
4+
class MyDict(dict):
5+
pass
6+
7+
class TestDictSubclass(unittest.TestCase):
8+
def test_get_path_with_subclass(self):
9+
# Test getPath with a dict subclass
10+
attributes = MyDict({"user": MyDict({"id": "123", "name": "John"})})
11+
12+
self.assertEqual(getPath(attributes, "user.id"), "123")
13+
self.assertEqual(getPath(attributes, "user.name"), "John")
14+
self.assertEqual(getPath(attributes, "user.nonexistent"), None)
15+
16+
def test_eval_condition_with_subclass(self):
17+
# Test evalCondition with a dict subclass
18+
attributes = MyDict({"company": "GrowthBook", "meta": MyDict({"plan": "pro"})})
19+
20+
# Simple condition
21+
condition = {"company": "GrowthBook"}
22+
self.assertTrue(evalCondition(attributes, condition))
23+
24+
# Nested condition using getPath (indirectly)
25+
condition = {"meta.plan": "pro"}
26+
self.assertTrue(evalCondition(attributes, condition))
27+
28+
# Condition failing
29+
condition = {"meta.plan": "free"}
30+
self.assertFalse(evalCondition(attributes, condition))
31+
32+
if __name__ == '__main__':
33+
unittest.main()

0 commit comments

Comments
 (0)