Skip to content

Commit a698e36

Browse files
kp-catadams85
andauthored
more config v6 tests (#61)
* 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]>
1 parent 5c6f623 commit a698e36

20 files changed

+2950
-65
lines changed

configcatclient/config.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import sys
2+
13
from enum import IntEnum
24

35
CONFIG_FILE_NAME = 'config_v6'
@@ -61,6 +63,24 @@
6163
UNSUPPORTED_VALUE = 'unsupported_value'
6264

6365

66+
def is_type_mismatch(value, py_type):
67+
is_float_int_mismatch = \
68+
(type(value) is float and py_type is int) or \
69+
(type(value) is int and py_type is float)
70+
71+
# On Python 2.7, ignore the type mismatch between str and unicode.
72+
# (ignore warning: unicode is undefined in Python 3)
73+
is_str_unicode_mismatch = \
74+
(sys.version_info[0] == 2 and type(value) is unicode and py_type is str) or \
75+
(sys.version_info[0] == 2 and type(value) is str and py_type is unicode) # noqa: F821
76+
77+
if type(value) is not py_type:
78+
if not is_float_int_mismatch and not is_str_unicode_mismatch:
79+
return True
80+
81+
return False
82+
83+
6484
def get_value(dictionary, setting_type):
6585
value_descriptor = dictionary.get(VALUE)
6686
if value_descriptor is None:
@@ -74,8 +94,8 @@ def get_value(dictionary, setting_type):
7494
raise ValueError('Unsupported setting type')
7595

7696
value = value_descriptor.get(expected_value_type)
77-
if value is None:
78-
raise ValueError('Setting value is not of the expected type %s' % expected_py_type)
97+
if value is None or is_type_mismatch(value, expected_py_type):
98+
raise ValueError("Setting value is not of the expected type %s" % expected_py_type)
7999

80100
return value
81101

configcatclient/configcatclient.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import logging
2-
import sys
32
from threading import Lock
43

54
from . import utils
65
from .configservice import ConfigService
7-
from .config import TARGETING_RULES, VARIATION_ID, PERCENTAGE_OPTIONS, FEATURE_FLAGS, SERVED_VALUE, SETTING_TYPE
6+
from .config import TARGETING_RULES, VARIATION_ID, PERCENTAGE_OPTIONS, FEATURE_FLAGS, SERVED_VALUE, SETTING_TYPE, \
7+
is_type_mismatch
88
from .evaluationdetails import EvaluationDetails
99
from .evaluationlogbuilder import EvaluationLogBuilder
1010
from .interfaces import ConfigCatClientException
@@ -374,23 +374,12 @@ def _get_config(self):
374374

375375
return self._config_service.get_config()
376376

377-
def _check_type_missmatch(self, value, default_value):
378-
is_float_int_missmatch = \
379-
(type(value) is float and type(default_value) is int) or \
380-
(type(value) is int and type(default_value) is float)
381-
382-
# On Python 2.7, do not log a warning if the type missmatch is between str and unicode.
383-
# (ignore warning: unicode is undefined in Python 3)
384-
is_str_unicode_missmatch = \
385-
(sys.version_info[0] == 2 and type(value) is unicode and type(default_value) is str) or \
386-
(sys.version_info[0] == 2 and type(value) is str and type(default_value) is unicode) # noqa: F821
387-
388-
if default_value is not None and type(value) is not type(default_value):
389-
if not is_float_int_missmatch and not is_str_unicode_missmatch:
390-
self.log.warning("The type of a setting does not match the type of the specified default value (%s). "
391-
"Setting's type was %s but the default value's type was %s. "
392-
"Please make sure that using a default value not matching the setting's type was intended." %
393-
(default_value, type(value), type(default_value)), event_id=4002)
377+
def _check_type_mismatch(self, value, default_value):
378+
if default_value is not None and is_type_mismatch(value, type(default_value)):
379+
self.log.warning("The type of a setting does not match the type of the specified default value (%s). "
380+
"Setting's type was %s but the default value's type was %s. "
381+
"Please make sure that using a default value not matching the setting's type was intended." %
382+
(default_value, type(value), type(default_value)), event_id=4002)
394383

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

409-
self._check_type_missmatch(value, default_value)
398+
self._check_type_mismatch(value, default_value)
410399

411400
if log_builder:
412401
self.log.info(str(log_builder), event_id=5000)

configcatclient/configfetcher.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ def _fetch(self, etag): # noqa: C901
201201
self.log.error(error, *error_args, event_id=1102)
202202
return FetchResponse.failure(Logger.format(error, error_args), True)
203203
except Exception as e:
204-
error = 'Unexpected error occurred while trying to fetch config JSON.'
204+
error = 'Unexpected error occurred while trying to fetch config JSON. It is most likely due to a local network ' \
205+
'issue. Please make sure your application can reach the ConfigCat CDN servers (or your proxy server) ' \
206+
'over HTTP.'
205207
self.log.exception(error, event_id=1103)
206208
return FetchResponse.failure(Logger.format(error, (), e), True)

configcatclient/localfiledatasource.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import codecs
12
import sys
23

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

2021

22+
def open_file(file_path, mode='r'):
23+
# Python 2.7, utf-8 is not supported in open() function
24+
if sys.version_info[0] == 2:
25+
return codecs.open(file_path, mode, encoding='utf-8')
26+
else:
27+
return open(file_path, mode, encoding='utf-8')
28+
29+
2130
class LocalFileDataSource(OverrideDataSource):
2231
def __init__(self, file_path, override_behaviour, log):
2332
OverrideDataSource.__init__(self, override_behaviour=override_behaviour)
@@ -42,7 +51,7 @@ def _reload_file_content(self): # noqa: C901
4251
stamp = os.stat(self._file_path).st_mtime
4352
if stamp != self._cached_file_stamp:
4453
self._cached_file_stamp = stamp
45-
with open(self._file_path) as file:
54+
with open_file(self._file_path) as file:
4655
data = json.load(file)
4756

4857
if sys.version_info[0] == 2:

configcatclient/rolloutevaluator.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
import hashlib
4+
import math
45
import sys
56
import semver
67

@@ -152,6 +153,19 @@ def _user_attribute_value_to_string(self, value):
152153
value = self._get_user_attribute_value_as_seconds_since_epoch(value)
153154
elif isinstance(value, list):
154155
value = self._get_user_attribute_value_as_string_list(value)
156+
return json.dumps(value, ensure_ascii=False, separators=(',', ':')) # Convert the list to a JSON string
157+
158+
if isinstance(value, float):
159+
if math.isnan(value):
160+
return 'NaN'
161+
if value == float('inf'):
162+
return 'Infinity'
163+
if value == float('-inf'):
164+
return '-Infinity'
165+
if 'e' in str(value):
166+
return str(value)
167+
if value.is_integer():
168+
return str(int(value))
155169

156170
return str(value)
157171

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

263-
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
277+
# Unicode fix on Python 2.7
278+
if sys.version_info[0] == 2:
279+
try:
280+
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
281+
except Exception:
282+
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).decode('utf-8').encode(
283+
'utf-8')
284+
else:
285+
hash_candidate = ('%s%s' % (key, self._user_attribute_value_to_string(user_key))).encode('utf-8')
264286
hash_val = int(hashlib.sha1(hash_candidate).hexdigest()[:7], 16) % 100
265287

266288
bucket = 0
@@ -317,7 +339,9 @@ def _evaluate_conditions(self, conditions, context, salt, config, log_builder, v
317339
result, error = self._evaluate_segment_condition(segment_condition, context, salt, log_builder)
318340
if log_builder:
319341
if len(conditions) > 1:
320-
log_builder.append(' => {}'.format('true' if result else 'false'))
342+
if error is None:
343+
log_builder.append(' ')
344+
log_builder.append('=> {}'.format('true' if result else 'false'))
321345
if not result:
322346
log_builder.append(', skipping the remaining AND conditions')
323347
elif error is None:
@@ -328,6 +352,14 @@ def _evaluate_conditions(self, conditions, context, salt, config, log_builder, v
328352
break
329353
elif prerequisite_flag_condition is not None:
330354
result = self._evaluate_prerequisite_flag_condition(prerequisite_flag_condition, context, config, log_builder)
355+
if log_builder:
356+
if len(conditions) > 1:
357+
log_builder.append(' => {}'.format('true' if result else 'false'))
358+
if not result:
359+
log_builder.append(', skipping the remaining AND conditions')
360+
elif error is None:
361+
log_builder.new_line()
362+
331363
if not result:
332364
condition_result = False
333365
break
@@ -363,13 +395,12 @@ def _evaluate_prerequisite_flag_condition(self, prerequisite_flag_condition, con
363395
prerequisite_flag_setting_type = settings[prerequisite_key].get(SETTING_TYPE)
364396
prerequisite_comparison_value_type = get_value_type(prerequisite_flag_condition)
365397

398+
prerequisite_comparison_value = get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)
399+
366400
# Type mismatch check
367401
if prerequisite_comparison_value_type != SettingType.to_type(prerequisite_flag_setting_type):
368-
raise ValueError("Type mismatch between comparison value type %s and type %s of prerequisite flag '%s'" %
369-
(prerequisite_comparison_value_type, SettingType.to_type(prerequisite_flag_setting_type),
370-
prerequisite_key))
371-
372-
prerequisite_comparison_value = get_value(prerequisite_flag_condition, prerequisite_flag_setting_type)
402+
raise ValueError("Type mismatch between comparison value '%s' and prerequisite flag '%s'" %
403+
(prerequisite_comparison_value, prerequisite_key))
373404

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

411442
if log_builder:
412443
log_builder.append('%s.' % ('true' if prerequisite_condition_result else 'false'))
413-
log_builder.decrease_indent().new_line(')').new_line()
444+
log_builder.decrease_indent().new_line(')')
414445

415446
return prerequisite_condition_result
416447

@@ -531,7 +562,7 @@ def _evaluate_user_condition(self, user_condition, context, context_salt, salt,
531562
return False, error
532563

533564
user_value = user.get_attribute(comparison_attribute)
534-
if user_value is None or not user_value:
565+
if user_value is None or (not user_value and not isinstance(user_value, list)):
535566
self.log.warning('Cannot evaluate condition (%s) for setting \'%s\' '
536567
'(the User.%s attribute is missing). You should set the User.%s attribute in order to make '
537568
'targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/',

configcatclient/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@ def serializer(obj):
8686
dump.update(self.__custom)
8787

8888
filtered_dump = OrderedDict([(k, v) for k, v in dump.items() if v is not None])
89-
return json.dumps(filtered_dump, separators=(',', ':'), default=serializer)
89+
return json.dumps(filtered_dump, ensure_ascii=False, separators=(',', ':'), default=serializer)

configcatclient/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CONFIGCATCLIENT_VERSION = "9.0.1"
1+
CONFIGCATCLIENT_VERSION = "9.0.2"

0 commit comments

Comments
 (0)