Skip to content

Commit 1d8b273

Browse files
authored
perf: Speed up IN segment condition evaluation (#295)
1 parent 6237c31 commit 1d8b273

4 files changed

Lines changed: 31 additions & 18 deletions

File tree

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "tests/engine_tests/engine-test-data"]
22
path = tests/engine_tests/engine-test-data
33
url = https://github.com/flagsmith/engine-test-data.git
4-
tag = v3.5.0
4+
tag = v3.6.0

flag_engine/segments/evaluator.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -279,23 +279,9 @@ def context_matches_condition(
279279
context_value = get_context_value(context, condition_property)
280280

281281
if condition_operator == constants.IN:
282-
if isinstance(segment_value := condition["value"], list):
283-
in_values = segment_value
284-
else:
285-
try:
286-
in_values = json.loads(segment_value)
287-
# Only accept JSON lists.
288-
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
289-
# but we aim to ditch the pydantic dependency in the future.
290-
if not isinstance(in_values, list):
291-
raise ValueError
292-
except ValueError:
293-
in_values = segment_value.split(",")
294-
in_values = [str(value) for value in in_values]
282+
in_values = _get_in_values(condition["value"])
295283
# Guard against comparing boolean values to numeric strings.
296-
if isinstance(context_value, int) and not (
297-
context_value is True or context_value is False
298-
):
284+
if type(context_value) is int:
299285
context_value = str(context_value)
300286
return context_value in in_values
301287

@@ -348,6 +334,30 @@ def _matches_context_value(
348334
return False
349335

350336

337+
@lru_cache(maxsize=1024)
338+
def _parse_in_values_str(segment_value: str) -> frozenset[str]:
339+
"""
340+
Parse a string-form IN condition value into a frozenset of strings.
341+
A bracketed value is tried as JSON first (with CSV fallback on parse
342+
error); anything else is split on commas directly.
343+
"""
344+
if segment_value.startswith("["):
345+
try:
346+
parsed: list[typing.Any] = json.loads(segment_value)
347+
except ValueError:
348+
return frozenset(segment_value.split(","))
349+
return frozenset(v if type(v) is str else str(v) for v in parsed)
350+
return frozenset(segment_value.split(","))
351+
352+
353+
def _get_in_values(
354+
segment_value: typing.Union[str, list[typing.Any]],
355+
) -> frozenset[str]:
356+
if isinstance(segment_value, list):
357+
return frozenset(v if type(v) is str else str(v) for v in segment_value)
358+
return _parse_in_values_str(segment_value)
359+
360+
351361
def _evaluate_not_contains(
352362
segment_value: typing.Optional[str],
353363
context_value: ContextValue,

tests/engine_tests/test_engine.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def _extract_benchmark_contexts(
3636
for file_path in [
3737
"test_0cfd0d72-4de4-4ed7-9cfb-d80dc3dacead__default.json",
3838
"test_1bde8445-ca19-4bda-a9d5-3543a800fc0f__context_values.json",
39+
"test_in_condition_json_array_format__should_match.jsonc",
40+
"test_in_condition_numeric_comma_separated__should_match.jsonc",
41+
"test_in_condition_array_matching_value__should_match.jsonc",
3942
]:
4043
yield pyjson5.loads((test_cases_dir_path / file_path).read_text())["context"]
4144

0 commit comments

Comments
 (0)