Skip to content

Commit 129108b

Browse files
committed
support native lists as condition values for the IN operator
1 parent 7fb2748 commit 129108b

File tree

5 files changed

+164
-56
lines changed

5 files changed

+164
-56
lines changed

flag_engine/context/mappers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def _map_identity_overrides_to_segment_contexts(
122122
{
123123
"property": "$.identity.identifier",
124124
"operator": "IN",
125-
"value": ",".join(identifiers),
125+
"value": identifiers,
126126
}
127127
],
128128
}

flag_engine/context/types.py

Lines changed: 15 additions & 6 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/features-contexts-in-eval-context-schema/sdk/evaluation-context.json
3+
# timestamp: 2025-08-15T18:12:12+00:00
44

55
from __future__ import annotations
66

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

99
from typing_extensions import NotRequired
1010

@@ -24,15 +24,24 @@ class FeatureValue(TypedDict):
2424
class IdentityContext(TypedDict):
2525
identifier: str
2626
key: str
27-
traits: NotRequired[Dict[str, Optional[Union[str, float, bool]]]]
27+
traits: NotRequired[Mapping[str, Optional[Union[str, float, bool]]]]
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: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
EvaluationContext,
1313
FeatureContext,
1414
SegmentCondition,
15-
SegmentContext,
16-
SegmentRule,
1715
)
16+
from flag_engine.context.types import SegmentCondition1 as SegmentConditionWithStrValue
17+
from flag_engine.context.types import SegmentContext, SegmentRule
1818
from flag_engine.environments.models import EnvironmentModel
1919
from flag_engine.identities.models import IdentityModel
2020
from flag_engine.identities.traits.types import ContextValue
@@ -227,6 +227,22 @@ def context_matches_condition(
227227
else None
228228
)
229229

230+
if condition["operator"] == constants.IN:
231+
in_values = (
232+
condition_value
233+
if isinstance(condition_value := condition["value"], list)
234+
else condition_value.split(",")
235+
)
236+
if isinstance(context_value, int) and not any(
237+
context_value is x for x in (False, True)
238+
):
239+
return str(context_value) in in_values
240+
return context_value in in_values
241+
242+
# mypy is too inflexible here, we know that, at this point,
243+
# the condition value is not a list[str]
244+
condition = typing.cast(SegmentConditionWithStrValue, condition)
245+
230246
if condition["operator"] == constants.PERCENTAGE_SPLIT:
231247
if context_value is not None:
232248
object_ids = [segment_key, context_value]
@@ -272,7 +288,7 @@ def get_context_value(
272288

273289

274290
def _matches_context_value(
275-
condition: SegmentCondition,
291+
condition: SegmentConditionWithStrValue,
276292
context_value: ContextValue,
277293
) -> bool:
278294
if matcher := MATCHERS_BY_OPERATOR.get(condition["operator"]):
@@ -318,19 +334,6 @@ def _evaluate_modulo(
318334
return context_value % divisor == remainder
319335

320336

321-
def _evaluate_in(
322-
segment_value: typing.Optional[str], context_value: ContextValue
323-
) -> bool:
324-
if segment_value:
325-
if isinstance(context_value, str):
326-
return context_value in segment_value.split(",")
327-
if isinstance(context_value, int) and not any(
328-
context_value is x for x in (False, True)
329-
):
330-
return str(context_value) in segment_value.split(",")
331-
return False
332-
333-
334337
def _context_value_typed(
335338
func: typing.Callable[..., bool],
336339
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
@@ -357,7 +360,6 @@ def inner(
357360
constants.NOT_CONTAINS: _evaluate_not_contains,
358361
constants.REGEX: _evaluate_regex,
359362
constants.MODULO: _evaluate_modulo,
360-
constants.IN: _evaluate_in,
361363
constants.EQUAL: _context_value_typed(operator.eq),
362364
constants.GREATER_THAN: _context_value_typed(operator.gt),
363365
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),

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)