Skip to content

Commit

Permalink
more config v6 tests (#61)
Browse files Browse the repository at this point in the history
* trimming tests + attribute tests

* lint fix

* Config V1 matrix tests

* user's to_string unicode support

* LocalFileDatasource: unicode support on windows

* test fix

* setting type mismatch test + exceptions + log update

* python 2.7 support

* Apply suggestions from code review

Co-authored-by: adams85 <[email protected]>

* typo fix

* bump version to 9.0.2

---------

Co-authored-by: adams85 <[email protected]>
  • Loading branch information
kp-cat and adams85 authored Feb 7, 2024
1 parent 5c6f623 commit a698e36
Show file tree
Hide file tree
Showing 20 changed files with 2,950 additions and 65 deletions.
24 changes: 22 additions & 2 deletions configcatclient/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

from enum import IntEnum

CONFIG_FILE_NAME = 'config_v6'
Expand Down Expand Up @@ -61,6 +63,24 @@
UNSUPPORTED_VALUE = 'unsupported_value'


def is_type_mismatch(value, py_type):
is_float_int_mismatch = \
(type(value) is float and py_type is int) or \
(type(value) is int and py_type is float)

# On Python 2.7, ignore the type mismatch between str and unicode.
# (ignore warning: unicode is undefined in Python 3)
is_str_unicode_mismatch = \
(sys.version_info[0] == 2 and type(value) is unicode and py_type is str) or \
(sys.version_info[0] == 2 and type(value) is str and py_type is unicode) # noqa: F821

if type(value) is not py_type:
if not is_float_int_mismatch and not is_str_unicode_mismatch:
return True

return False


def get_value(dictionary, setting_type):
value_descriptor = dictionary.get(VALUE)
if value_descriptor is None:
Expand All @@ -74,8 +94,8 @@ def get_value(dictionary, setting_type):
raise ValueError('Unsupported setting type')

value = value_descriptor.get(expected_value_type)
if value is None:
raise ValueError('Setting value is not of the expected type %s' % expected_py_type)
if value is None or is_type_mismatch(value, expected_py_type):
raise ValueError("Setting value is not of the expected type %s" % expected_py_type)

return value

Expand Down
29 changes: 9 additions & 20 deletions configcatclient/configcatclient.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging
import sys
from threading import Lock

from . import utils
from .configservice import ConfigService
from .config import TARGETING_RULES, VARIATION_ID, PERCENTAGE_OPTIONS, FEATURE_FLAGS, SERVED_VALUE, SETTING_TYPE
from .config import TARGETING_RULES, VARIATION_ID, PERCENTAGE_OPTIONS, FEATURE_FLAGS, SERVED_VALUE, SETTING_TYPE, \
is_type_mismatch
from .evaluationdetails import EvaluationDetails
from .evaluationlogbuilder import EvaluationLogBuilder
from .interfaces import ConfigCatClientException
Expand Down Expand Up @@ -374,23 +374,12 @@ def _get_config(self):

return self._config_service.get_config()

def _check_type_missmatch(self, value, default_value):
is_float_int_missmatch = \
(type(value) is float and type(default_value) is int) or \
(type(value) is int and type(default_value) is float)

# On Python 2.7, do not log a warning if the type missmatch is between str and unicode.
# (ignore warning: unicode is undefined in Python 3)
is_str_unicode_missmatch = \
(sys.version_info[0] == 2 and type(value) is unicode and type(default_value) is str) or \
(sys.version_info[0] == 2 and type(value) is str and type(default_value) is unicode) # noqa: F821

if default_value is not None and type(value) is not type(default_value):
if not is_float_int_missmatch and not is_str_unicode_missmatch:
self.log.warning("The type of a setting does not match the type of the specified default value (%s). "
"Setting's type was %s but the default value's type was %s. "
"Please make sure that using a default value not matching the setting's type was intended." %
(default_value, type(value), type(default_value)), event_id=4002)
def _check_type_mismatch(self, value, default_value):
if default_value is not None and is_type_mismatch(value, type(default_value)):
self.log.warning("The type of a setting does not match the type of the specified default value (%s). "
"Setting's type was %s but the default value's type was %s. "
"Please make sure that using a default value not matching the setting's type was intended." %
(default_value, type(value), type(default_value)), event_id=4002)

def _evaluate(self, key, user, default_value, default_variation_id, config, fetch_time):
user = user if user is not None else self._default_user
Expand All @@ -406,7 +395,7 @@ def _evaluate(self, key, user, default_value, default_variation_id, config, fetc
config=config,
log_builder=log_builder)

self._check_type_missmatch(value, default_value)
self._check_type_mismatch(value, default_value)

if log_builder:
self.log.info(str(log_builder), event_id=5000)
Expand Down
4 changes: 3 additions & 1 deletion configcatclient/configfetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ def _fetch(self, etag): # noqa: C901
self.log.error(error, *error_args, event_id=1102)
return FetchResponse.failure(Logger.format(error, error_args), True)
except Exception as e:
error = 'Unexpected error occurred while trying to fetch config JSON.'
error = 'Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network ' \
'issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) ' \
'over HTTP.'
self.log.exception(error, event_id=1103)
return FetchResponse.failure(Logger.format(error, (), e), True)
11 changes: 10 additions & 1 deletion configcatclient/localfiledatasource.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import codecs
import sys

from .config import extend_config_with_inline_salt_and_segment, VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, \
Expand All @@ -18,6 +19,14 @@ def create_data_source(self, log):
return LocalFileDataSource(self.file_path, self.override_behaviour, log)


def open_file(file_path, mode='r'):
# Python 2.7, utf-8 is not supported in open() function
if sys.version_info[0] == 2:
return codecs.open(file_path, mode, encoding='utf-8')
else:
return open(file_path, mode, encoding='utf-8')


class LocalFileDataSource(OverrideDataSource):
def __init__(self, file_path, override_behaviour, log):
OverrideDataSource.__init__(self, override_behaviour=override_behaviour)
Expand All @@ -42,7 +51,7 @@ def _reload_file_content(self): # noqa: C901
stamp = os.stat(self._file_path).st_mtime
if stamp != self._cached_file_stamp:
self._cached_file_stamp = stamp
with open(self._file_path) as file:
with open_file(self._file_path) as file:
data = json.load(file)

if sys.version_info[0] == 2:
Expand Down
49 changes: 40 additions & 9 deletions configcatclient/rolloutevaluator.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json

import hashlib
import math
import sys
import semver

Expand Down Expand Up @@ -152,6 +153,19 @@ def _user_attribute_value_to_string(self, value):
value = self._get_user_attribute_value_as_seconds_since_epoch(value)
elif isinstance(value, list):
value = self._get_user_attribute_value_as_string_list(value)
return json.dumps(value, ensure_ascii=False, separators=(',', ':')) # Convert the list to a JSON string

if isinstance(value, float):
if math.isnan(value):
return 'NaN'
if value == float('inf'):
return 'Infinity'
if value == float('-inf'):
return '-Infinity'
if 'e' in str(value):
return str(value)
if value.is_integer():
return str(int(value))

return str(value)

Expand Down Expand Up @@ -260,7 +274,15 @@ def _evaluate_percentage_options(self, percentage_options, context, percentage_r
'Skipping %% options because the User.%s attribute is missing.' % user_attribute_name)
return False, None, None, None

hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
# Unicode fix on Python 2.7
if sys.version_info[0] == 2:
try:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
except Exception:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).decode('utf-8').encode(
'utf-8')
else:
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
hash_val = int(hashlib.sha1(hash_candidate).hexdigest()[:7], 16) % 100

bucket = 0
Expand Down Expand Up @@ -317,7 +339,9 @@ def _evaluate_conditions(self, conditions, context, salt, config, log_builder, v
result, error = self._evaluate_segment_condition(segment_condition, context, salt, log_builder)
if log_builder:
if len(conditions) > 1:
log_builder.append(' => {}'.format('true' if result else 'false'))
if error is None:
log_builder.append(' ')
log_builder.append('=> {}'.format('true' if result else 'false'))
if not result:
log_builder.append(', skipping the remaining AND conditions')
elif error is None:
Expand All @@ -328,6 +352,14 @@ def _evaluate_conditions(self, conditions, context, salt, config, log_builder, v
break
elif prerequisite_flag_condition is not None:
result = self._evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder)
if log_builder:
if len(conditions) > 1:
log_builder.append(' => {}'.format('true' if result else 'false'))
if not result:
log_builder.append(', skipping the remaining AND conditions')
elif error is None:
log_builder.new_line()

if not result:
condition_result = False
break
Expand Down Expand Up @@ -363,13 +395,12 @@ def _evaluate_prerequisite_flag_condition(self, prerequisite_flag_condition, con
prerequisite_flag_setting_type = settings[prerequisite_key].get(SETTING_TYPE)
prerequisite_comparison_value_type = get_value_type(prerequisite_flag_condition)

prerequisite_comparison_value = get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)

# Type mismatch check
if prerequisite_comparison_value_type != SettingType.to_type(prerequisite_flag_setting_type):
raise ValueError("Type mismatch between comparison value type %s and type %s of prerequisite flag '%s'" %
(prerequisite_comparison_value_type, SettingType.to_type(prerequisite_flag_setting_type),
prerequisite_key))

prerequisite_comparison_value = get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)
raise ValueError("Type mismatch between comparison value '%s' and prerequisite flag '%s'" %
(prerequisite_comparison_value, prerequisite_key))

prerequisite_condition = ("Flag '%s' %s '%s'" %
(prerequisite_key, PREREQUISITE_COMPARATOR_TEXTS[prerequisite_comparator],
Expand Down Expand Up @@ -410,7 +441,7 @@ def _evaluate_prerequisite_flag_condition(self, prerequisite_flag_condition, con

if log_builder:
log_builder.append('%s.' % ('true' if prerequisite_condition_result else 'false'))
log_builder.decrease_indent().new_line(')').new_line()
log_builder.decrease_indent().new_line(')')

return prerequisite_condition_result

Expand Down Expand Up @@ -531,7 +562,7 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
return False, error

user_value = user.get_attribute(comparison_attribute)
if user_value is None or not user_value:
if user_value is None or (not user_value and not isinstance(user_value, list)):
self.log.warning('Cannot evaluate condition (%s) for setting \'%s\' '
'(the User.%s attribute is missing). You should set the User.%s attribute in order to make '
'targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/',
Expand Down
2 changes: 1 addition & 1 deletion configcatclient/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ def serializer(obj):
dump.update(self.__custom)

filtered_dump = OrderedDict([(k, v) for k, v in dump.items() if v is not None])
return json.dumps(filtered_dump, separators=(',', ':'), default=serializer)
return json.dumps(filtered_dump, ensure_ascii=False, separators=(',', ':'), default=serializer)
2 changes: 1 addition & 1 deletion configcatclient/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
CONFIGCATCLIENT_VERSION = "9.0.1"
CONFIGCATCLIENT_VERSION = "9.0.2"
Loading

0 comments on commit a698e36

Please sign in to comment.