-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat (audience match types): Update condition evaluator for new audience match types #146
feat (audience match types): Update condition evaluator for new audience match types #146
Conversation
|
||
def assertStrictFalse(self, to_assert): | ||
self.assertIs(to_assert, False) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assertTrue and assertFalse check for truthy and falsy values.
Added these to ensure that we don't mistake null for false
…ience-match-type-condition-evaluator-2
.github/pull_request_template.rst
Outdated
@@ -0,0 +1,15 @@ | |||
Summary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why this changes are coming in this PR? @oakbani
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated base branch with master
I think compat tests are now available. You can update the test plan. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is sort of a big change. Will try and finish review today.
optimizely/helpers/condition.py
Outdated
result = self.evaluate(conditions[0], leaf_evaluator) | ||
return None if result is None else not result | ||
|
||
DEFAULT_OPERATOR_TYPES = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Put these dicts after line 33.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aliabbasrizvi that might work better if these methods are all declared with @staticmethod
.
Hmm, since ConditionTreeEvaluator
actually doesn't contain any state, I wonder if we should just break it out into a separate module with top-level functions. As in the JS SDK. This might also be a preferable way to namespace when we add support for other audience condition types, other condition trees (e.g. audience combinations), ...
@oakbani what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. I don't think DEFAULT_...
needs to be in the name of this enum.
And arguably we could get rid of this enum in favor of EVALUATORS_BY_OPERATOR_TYPE
!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering if we should create condition as a separate module and keep each of these classes in separate files for better organization. @nchilada Yeah we can make them static.
@aliabbasrizvi What do you suggest? The changes will propagate to project config and combinations work(which has been done). I hope we are not in an urgency to merge this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to make them separate modules with their own top-level functions. From a code organization standpoint I am ok if all classes stay in this file itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @oakbani! Thanks for waiting so long 😬
I've spent too much time in JavaScript-land but this seems like really good Python. I only left minor feedback, the most obnoxious being a potential reorganization to reduce the amount of class-based namespacing and more closely match the JS SDK.
I'll look at the unit tests in my next pass of review!
optimizely/helpers/condition.py
Outdated
@@ -159,7 +392,12 @@ def _audience_condition_deserializer(obj_dict): | |||
Returns: | |||
List consisting of condition key and corresponding value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of date!
optimizely/helpers/condition.py
Outdated
result = self.evaluate(conditions[0], leaf_evaluator) | ||
return None if result is None else not result | ||
|
||
DEFAULT_OPERATOR_TYPES = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aliabbasrizvi that might work better if these methods are all declared with @staticmethod
.
Hmm, since ConditionTreeEvaluator
actually doesn't contain any state, I wonder if we should just break it out into a separate module with top-level functions. As in the JS SDK. This might also be a preferable way to namespace when we add support for other audience condition types, other condition trees (e.g. audience combinations), ...
@oakbani what do you think?
optimizely/helpers/condition.py
Outdated
|
||
# Compare types if one of the values is bool because bool is a subclass on Integer | ||
if isinstance(first_val, bool) or isinstance(second_val, bool): | ||
return first_val_type == second_val_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simpler, I think, and matches the structure of the numeric block:
if isinstance(first_val, bool) and isinstance(second_val, bool):
return True
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case if only 1 of values is a bool type and the other is float or int
it will return true in the following if block as bool is a subclass of int
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, you're right. So tricky! I haven't looked at the unit tests yet but hopefully we have test cases for this. The compatibility suite can't test the specifics of Python data types
optimizely/helpers/condition.py
Outdated
if isinstance(first_val, string_types) and isinstance(second_val, string_types): | ||
return True | ||
|
||
# Compare types if one of the values is bool because bool is a subclass on Integer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think the key detail here is that we have to check bool
before we check for arbitrary numbers
. Could we find a way to mention this in the comment?
|
||
return condition_value in user_value | ||
|
||
EVALUATORS_BY_MATCH_TYPE = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing @aliabbasrizvi would want this, too, to be declared above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose can't declare it before defining evaluator methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good. Left some comments.
optimizely/helpers/condition.py
Outdated
Boolean: | ||
- True if all operands evaluate to True | ||
- False if a single operand evaluates to False | ||
None: if conditions couldn't be evaluated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Might be useful to leave a comment here explaining when this would be the case. Thoughts on example @nchilada ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. Would definitely get verbose. Perhaps we could just force ourselves to be diligent in documenting the new, collective behavior somewhere else? E.g. in the KB or at the very top of the condition tree module (rather than separately for each operator's evaluator)
optimizely/helpers/condition.py
Outdated
- True if any operand evaluates to True | ||
- False if all operands evaluate to False | ||
None: if conditions couldn't be evaluated | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Remove this extraneous line.
optimizely/helpers/condition.py
Outdated
- True if the operand evaluates to False | ||
- False if the operand evaluates to True | ||
None: if conditions is empty or condition couldn't be evaluated | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Remove this extraneous line.
optimizely/helpers/condition.py
Outdated
result = self.evaluate(conditions[0], leaf_evaluator) | ||
return None if result is None else not result | ||
|
||
DEFAULT_OPERATOR_TYPES = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My preference is to make them separate modules with their own top-level functions. From a code organization standpoint I am ok if all classes stay in this file itself.
optimizely/helpers/condition.py
Outdated
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
if not self.is_finite(condition_value) or not self.is_finite(user_value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If self.is_finite(condition_value)
is False
, then that suggests a potential problem in the way the audience is set up.
@nchilada do we have log messages to help customers identify the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, not self.is_finite(condition_value)
is degenerate (though still worth checking).
We're refining our log messages as a separate initiative...it's looking like we're more concerned about logging problems that customers are actually expected to encounter, but we'll see. Can I add you as a reviewer on the other doc that I'm talking about?
|
||
return condition_value in user_value | ||
|
||
EVALUATORS_BY_MATCH_TYPE = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙇
'name': 'browser_type', | ||
'value': 'safari', | ||
'type': 'custom_attribute', | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. This indentation seems off.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! I'll try to skim through the tests late today.
if math.isnan(value) or math.isinf(value): | ||
return False | ||
|
||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for extracting this! As mentioned on a now-obsolete commit, can we
- enforce an absolute size limit of 1e53 if
isinstance(value, numbers.Integral)
- update
helpers.validator.is_attribute_valid
such that it callshelpers.validator.is_finite_number
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And @aliabbasrizvi's addendum:
Random. I am wondering if we should extend this validation i.e. the
is_finite
check to revenue and quantity values?
+1, is worth considering
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do this in a separate commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume by "separate commit" you mean "separate PR"? That's fine, thanks! Tracking in 3654
optimizely/helpers/condition.py
Outdated
|
||
# Compare types if one of the values is bool because bool is a subclass on Integer | ||
if isinstance(first_val, bool) or isinstance(second_val, bool): | ||
return first_val_type == second_val_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, you're right. So tricky! I haven't looked at the unit tests yet but hopefully we have test cases for this. The compatibility suite can't test the specifics of Python data types
optimizely/helpers/condition.py
Outdated
Boolean: | ||
- True if all operands evaluate to True | ||
- False if a single operand evaluates to False | ||
None: if conditions couldn't be evaluated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure. Would definitely get verbose. Perhaps we could just force ourselves to be diligent in documenting the new, collective behavior somewhere else? E.g. in the KB or at the very top of the condition tree module (rather than separately for each operator's evaluator)
optimizely/helpers/condition.py
Outdated
condition_value = self.condition_data[index][1] | ||
user_value = self.attributes.get(self.condition_data[index][0]) | ||
|
||
if not self.is_finite(condition_value) or not self.is_finite(user_value): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, not self.is_finite(condition_value)
is degenerate (though still worth checking).
We're refining our log messages as a separate initiative...it's looking like we're more concerned about logging problems that customers are actually expected to encounter, but we'll see. Can I add you as a reviewer on the other doc that I'm talking about?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @oakbani! Code looks good. I just have a couple minor requests for the test cases
class ConditionEvaluatorTests(base.BaseTest): | ||
exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] | ||
exact_string_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] | ||
exact_number_condition_list = [['lasers_count', 9000, 'custom_attribute', 'exact']] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also have a test for exact number match where the condition value is a float? I see the doubleCondition
above, but it looks like that's used for something else
def test_exact_number__returns_true__when_user_provided_value_is_equal_to_condition_value(self): | ||
|
||
evaluator = condition_helper.CustomAttributeConditionEvaluator( | ||
exact_number_condition_list, {'lasers_count': 9000} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also find a way to test with 9000L
in the case of Python 2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually...do we run our unit tests in both Python 2 and Python 3?? @aliabbasrizvi
|
||
self.assertIsNone(evaluator.evaluate(0)) | ||
|
||
def test_exact_number__returns_true__when_user_provided_value_is_equal_to_condition_value(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have two versions of this:
test_exact_int_returns_true__when_user_provided_value_is_equal_to_condition_value
test_exact_float_returns_true__when_user_provided_value_is_equal_to_condition_value
with the same assertions but different condition value types.
Same for most or all of the other exact_number
, greater_than
, and less_than
tests!
exact_bool_condition_list = [['did_register_user', False, 'custom_attribute', 'exact']] | ||
substring_condition_list = [['headline_text', 'buy now', 'custom_attribute', 'substring']] | ||
gt_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'gt']] | ||
lt_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'lt']] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also have int
versions of the above two conditions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stellar work. Thanks @oakbani!
Co-Authored-By: oakbani <owais.akbani92@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Requesting some minor changes.
optimizely/helpers/condition.py
Outdated
|
||
class ConditionalOperatorTypes(object): | ||
from .validator import is_finite_number, are_values_same_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. I personally prefer doing something like:
from . import validator
and then call the respective method.
optimizely/helpers/condition.py
Outdated
|
||
Args: | ||
condition: Integer representing the index of condition_data that needs to be used for comparison. | ||
value: Value to validate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Period (.) at the end of line. Please make sure to do so in all messages in this PR.
optimizely/helpers/condition.py
Outdated
|
||
Returns: | ||
Boolean indicating the result of comparing the condition value against the user attributes. | ||
Boolean: True if value is a string type, or a boolean, or is finite. Otherwise False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit. Same as above.
optimizely/helpers/condition.py
Outdated
|
||
Args: | ||
conditions: List of conditions ex: [operand_1, operand_2] | ||
index: Index of the condition to be evaluated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index is a built in Python method I think. Please rename to something else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optimizely/helpers/condition.py
Outdated
""" Evaluate the given exists match condition for the user attributes | ||
|
||
Args: | ||
index: Index of the condition to be evaluated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above. Please update all instances of index in this PR to be something different.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is actually clean after all!
@@ -120,13 +120,13 @@ def test_init(self): | |||
'11154', 'Test attribute users 1', | |||
'["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', | |||
conditionStructure=['and', ['or', ['or', 0]]], | |||
conditionList=[['test_attribute', 'test_value_1']] | |||
conditionList=[['test_attribute', 'test_value_1', 'custom_attribute', None]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We haven't updated project_config.py
code. How is this getting set to None?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have edited def _audience_condition_deserializer(obj_dict):
to add type and match to attribute node so that we use it in CustomAttributeConditionEvaluator - evaluate()
https://github.com/optimizely/python-sdk/pull/146/files#diff-54739e7fb9eb3054932a2f39734e8676R218-R232
…nfig for new audience match types (#147)
Summary
This adds support for new audience match type conditions to the condition evaluator:
Test Plan
Added new unit tests.
Ran compatibility suite tests for typed audiences