Skip to content

Commit 40c3adc

Browse files
committed
support list[str] for IN operator
1 parent 1eaca0a commit 40c3adc

File tree

6 files changed

+188
-88
lines changed

6 files changed

+188
-88
lines changed

flag_engine/context/types.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# generated by datamodel-codegen:
2-
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/features-contexts-in-eval-context-schema/sdk/evaluation-context.json # noqa: E501
3-
# timestamp: 2025-08-11T18:17:29+00:00
2+
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/support-str-array-for-in-condition/sdk/evaluation-context.json
3+
# timestamp: 2025-08-25T11:10:31+00:00
44

55
from __future__ import annotations
66

7-
from typing import Any, Dict, List, Optional, TypedDict
7+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
88

99
from typing_extensions import NotRequired
1010

@@ -27,12 +27,21 @@ class IdentityContext(TypedDict):
2727
traits: NotRequired[Dict[str, ContextValue]]
2828

2929

30-
class SegmentCondition(TypedDict):
31-
property: NotRequired[str]
30+
class SegmentCondition1(TypedDict):
31+
property: str
3232
operator: ConditionOperator
3333
value: str
3434

3535

36+
class SegmentCondition2(TypedDict):
37+
property: str
38+
operator: Literal["IN"]
39+
value: List[str]
40+
41+
42+
SegmentCondition = Union[SegmentCondition1, SegmentCondition2]
43+
44+
3645
class SegmentRule(TypedDict):
3746
type: RuleType
3847
conditions: NotRequired[List[SegmentCondition]]

flag_engine/segments/evaluator.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
EvaluationContext,
1515
FeatureContext,
1616
SegmentCondition,
17-
SegmentContext,
18-
SegmentRule,
1917
)
18+
from flag_engine.context.types import SegmentCondition1 as StrValueSegmentCondition
19+
from flag_engine.context.types import SegmentContext, SegmentRule
2020
from flag_engine.result.types import EvaluationResult, FlagResult, SegmentResult
2121
from flag_engine.segments import constants
2222
from flag_engine.segments.types import ConditionOperator, ContextValue, is_context_value
@@ -189,6 +189,29 @@ def context_matches_condition(
189189
else None
190190
)
191191

192+
if condition["operator"] == constants.IN:
193+
if isinstance(segment_value := condition["value"], list):
194+
in_values = segment_value
195+
else:
196+
try:
197+
in_values = json.loads(segment_value)
198+
# Only accept JSON lists.
199+
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
200+
# but we aim to ditch the pydantic dependency in the future.
201+
if not isinstance(in_values, list):
202+
raise ValueError
203+
in_values = [str(value) for value in in_values]
204+
except ValueError:
205+
in_values = segment_value.split(",")
206+
# Guard against comparing boolean values to numeric strings.
207+
if isinstance(context_value, int) and not any(
208+
context_value is x for x in (False, True)
209+
):
210+
context_value = str(context_value)
211+
return context_value in in_values
212+
213+
condition = typing.cast(StrValueSegmentCondition, condition)
214+
192215
if condition["operator"] == constants.PERCENTAGE_SPLIT:
193216
if context_value is not None:
194217
object_ids = [segment_key, context_value]
@@ -225,7 +248,7 @@ def get_context_value(
225248

226249

227250
def _matches_context_value(
228-
condition: SegmentCondition,
251+
condition: StrValueSegmentCondition,
229252
context_value: ContextValue,
230253
) -> bool:
231254
if matcher := MATCHERS_BY_OPERATOR.get(condition["operator"]):
@@ -271,29 +294,6 @@ def _evaluate_modulo(
271294
return context_value % divisor == remainder
272295

273296

274-
def _evaluate_in(
275-
segment_value: typing.Optional[str], context_value: ContextValue
276-
) -> bool:
277-
if segment_value:
278-
try:
279-
in_values = json.loads(segment_value)
280-
# Only accept JSON lists.
281-
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
282-
# but we aim to ditch the pydantic dependency in the future.
283-
if not isinstance(in_values, list):
284-
raise ValueError
285-
in_values = [str(value) for value in in_values]
286-
except ValueError:
287-
in_values = segment_value.split(",")
288-
# Guard against comparing boolean values to numeric strings.
289-
if isinstance(context_value, int) and not any(
290-
context_value is x for x in (False, True)
291-
):
292-
context_value = str(context_value)
293-
return context_value in in_values
294-
return False
295-
296-
297297
def _context_value_typed(
298298
func: typing.Callable[..., bool],
299299
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
@@ -320,7 +320,6 @@ def inner(
320320
constants.NOT_CONTAINS: _evaluate_not_contains,
321321
constants.REGEX: _evaluate_regex,
322322
constants.MODULO: _evaluate_modulo,
323-
constants.IN: _evaluate_in,
324323
constants.EQUAL: _context_value_typed(operator.eq),
325324
constants.GREATER_THAN: _context_value_typed(operator.gt),
326325
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),

tests/engine_tests/test_engine.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from flag_engine.context.types import (
1111
EvaluationContext,
1212
FeatureContext,
13-
SegmentCondition,
1413
SegmentRule,
1514
)
1615
from flag_engine.engine import get_evaluation_result
@@ -56,7 +55,7 @@ def _extract_segment_rules(rules: list[dict[str, typing.Any]]) -> list[SegmentRu
5655
SegmentRule(
5756
type=rule["type"],
5857
conditions=[
59-
SegmentCondition(
58+
dict(
6059
property=condition.get("property_"),
6160
operator=condition["operator"],
6261
value=condition["value"],

tests/unit/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def segment_condition(
2727
segment_condition_property: str,
2828
segment_condition_string_value: str,
2929
) -> SegmentCondition:
30-
return SegmentCondition(
30+
return dict(
3131
operator=constants.EQUAL,
3232
property=segment_condition_property,
3333
value=segment_condition_string_value,

tests/unit/segments/fixtures.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from flag_engine.context.types import SegmentCondition, SegmentContext, SegmentRule
1+
from flag_engine.context.types import SegmentContext, SegmentRule
22
from flag_engine.segments import constants
33

44
trait_key_1 = "email"
@@ -19,7 +19,7 @@
1919
SegmentRule(
2020
type=constants.ALL_RULE,
2121
conditions=[
22-
SegmentCondition(
22+
dict(
2323
operator=constants.EQUAL,
2424
property=trait_key_1,
2525
value=trait_value_1,
@@ -36,12 +36,12 @@
3636
SegmentRule(
3737
type=constants.ALL_RULE,
3838
conditions=[
39-
SegmentCondition(
39+
dict(
4040
operator=constants.EQUAL,
4141
property=trait_key_1,
4242
value=trait_value_1,
4343
),
44-
SegmentCondition(
44+
dict(
4545
operator=constants.EQUAL,
4646
property=trait_key_2,
4747
value=trait_value_2,
@@ -58,12 +58,12 @@
5858
SegmentRule(
5959
type=constants.ANY_RULE,
6060
conditions=[
61-
SegmentCondition(
61+
dict(
6262
operator=constants.EQUAL,
6363
property=trait_key_1,
6464
value=trait_value_1,
6565
),
66-
SegmentCondition(
66+
dict(
6767
operator=constants.EQUAL,
6868
property=trait_key_2,
6969
value=trait_value_2,
@@ -83,12 +83,12 @@
8383
SegmentRule(
8484
type=constants.ALL_RULE,
8585
conditions=[
86-
SegmentCondition(
86+
dict(
8787
operator=constants.EQUAL,
8888
property=trait_key_1,
8989
value=trait_value_1,
9090
),
91-
SegmentCondition(
91+
dict(
9292
operator=constants.EQUAL,
9393
property=trait_key_2,
9494
value=trait_value_2,
@@ -98,7 +98,7 @@
9898
SegmentRule(
9999
type=constants.ALL_RULE,
100100
conditions=[
101-
SegmentCondition(
101+
dict(
102102
operator=constants.EQUAL,
103103
property=trait_key_3,
104104
value=trait_value_3,
@@ -117,7 +117,7 @@
117117
SegmentRule(
118118
type=constants.ALL_RULE,
119119
conditions=[
120-
SegmentCondition(
120+
dict(
121121
operator=constants.EQUAL,
122122
property=trait_key_1,
123123
value=trait_value_1,
@@ -127,7 +127,7 @@
127127
SegmentRule(
128128
type=constants.ALL_RULE,
129129
conditions=[
130-
SegmentCondition(
130+
dict(
131131
operator=constants.EQUAL,
132132
property=trait_key_2,
133133
value=trait_value_2,
@@ -137,7 +137,7 @@
137137
SegmentRule(
138138
type=constants.ALL_RULE,
139139
conditions=[
140-
SegmentCondition(
140+
dict(
141141
operator=constants.EQUAL,
142142
property=trait_key_3,
143143
value=trait_value_3,

0 commit comments

Comments
 (0)