Skip to content

Commit 8e6ce66

Browse files
authored
Merge pull request #7 from mathspace/remove-target
Remove references to target_id and simplify flag evaluation signatures
2 parents c4ff6e9 + 606de6d commit 8e6ce66

File tree

7 files changed

+97
-110
lines changed

7 files changed

+97
-110
lines changed

src/feafea/__init__.py

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ def _hash_percent(s: str, seed: str = "") -> float:
3838
3939
This is not the most efficient hash function but it's stable.
4040
Stability of this hash function is CRUCIAL. It's used to hash feature flag
41-
names and target IDs to ensure consistent evaluation of feature flags across
41+
names and split targets to ensure consistent evaluation of feature flags across
4242
time, different instances, different python versions, operating systems and
43-
so on because the distribution of target IDs depend on it.
43+
so on because the distribution of split targets depend on it.
4444
"""
4545
return (
4646
int.from_bytes(
@@ -361,7 +361,7 @@ class _CompiledFilter:
361361
__slots__ = ("eval", "flag_refs", "rule_refs")
362362

363363
# The compiled filter function.
364-
eval: Callable[[TargetID, Attributes], bool]
364+
eval: Callable[[Attributes], bool]
365365
# The set of flag names referenced in the filter.
366366
flag_refs: set[str]
367367
# The set of rule names referenced in the filter.
@@ -564,7 +564,7 @@ def _pythonize(self, f: _ParsedFilter, defined_flags: dict[str, CompiledFlag], d
564564
# This will therefore never raise an exception.
565565
if self._ignore_undefined_refs and sym_name not in defined_flags:
566566
return "False"
567-
lhs = f"flags[{sym_name!r}].eval(target_id, attributes).variant"
567+
lhs = f"flags[{sym_name!r}].eval(attributes).variant"
568568
if op in {"IN", "NOTIN"}:
569569
return f"{lhs} {self._py_op_map[op]} sets[{arg2!r}]"
570570
else:
@@ -598,7 +598,7 @@ def _pythonize(self, f: _ParsedFilter, defined_flags: dict[str, CompiledFlag], d
598598
# exists. This will therefore never raise an exception.
599599
if self._ignore_undefined_refs and sym_name not in defined_rules:
600600
return "False"
601-
lhs = f"rules[{sym_name!r}].eval(target_id, attributes)"
601+
lhs = f"rules[{sym_name!r}].eval(attributes)"
602602
return f"{lhs} {self._py_op_map[op]} {arg2!r}"
603603
case _: # pragma: no cover
604604
assert False, "unreachable" # pragma: no cover
@@ -622,7 +622,7 @@ def compile(self, name: str, flags: dict[str, CompiledFlag], rules: dict[str, _C
622622
optimized = self._optimize(inlined)
623623
py_expr = self._pythonize(optimized, flags, rules)
624624
code_str = f"""
625-
def a(target_id, attributes):
625+
def a(attributes):
626626
return {py_expr}
627627
"""
628628
py_code = compile(code_str, "", "exec")
@@ -645,9 +645,6 @@ def a(target_id, attributes):
645645
_config_schema = json.load(f)
646646

647647

648-
type TargetID = str
649-
650-
651648
class _CompiledRule:
652649
"""
653650
Flag independent compiled rule that evaluates to a boolean indicating
@@ -656,7 +653,7 @@ class _CompiledRule:
656653

657654
__slots__ = ("name", "eval", "_compiled_filter")
658655
name: str
659-
eval: Callable[[TargetID, Attributes], bool]
656+
eval: Callable[[Attributes], bool]
660657
_compiled_filter: _CompiledFilter
661658

662659

@@ -668,7 +665,7 @@ class _CompiledFlagRule:
668665

669666
__slots__ = ("rule_name", "eval")
670667
rule_name: str
671-
eval: Callable[[TargetID, Attributes], Variant | None]
668+
eval: Callable[[Attributes], Variant | None]
672669

673670

674671
class CompiledFlag:
@@ -691,13 +688,12 @@ class CompiledFlag:
691688
_all_rules: dict[str, _CompiledRule]
692689
_all_flags: dict[str, CompiledFlag]
693690

694-
def eval(self, target_id: TargetID, attributes: Attributes) -> FlagEvaluation:
691+
def eval(self, attributes: Attributes) -> FlagEvaluation:
695692
e = FlagEvaluation()
696693
e.default = self.default
697694
e.flag = self.name
698-
e.target_id = target_id
699695
for rule in self._rules:
700-
v = rule.eval(target_id, attributes)
696+
v = rule.eval(attributes)
701697
if v is not None:
702698
e.rule = rule.rule_name
703699
e.variant = v
@@ -861,11 +857,11 @@ def from_dict(c: DictConfig, ignore_undefined_refs: bool = False) -> CompiledCon
861857
compiled_rule._compiled_filter = cf
862858
globals["match"] = cf.eval
863859
py_common += [f"attributes['__seed'] = {r.get('split_group', rule_name)!r}"]
864-
py_common += ["if not match(target_id, attributes): return None"]
860+
py_common += ["if not match(attributes): return None"]
865861

866862
# Compile the flag independent rule.
867863

868-
lines = ["def a(target_id, attributes):"]
864+
lines = ["def a(attributes):"]
869865
for line in py_common:
870866
line = re.sub(r"return None$", "return False", line)
871867
lines.append(f" {line}")
@@ -889,7 +885,7 @@ def from_dict(c: DictConfig, ignore_undefined_refs: bool = False) -> CompiledCon
889885
# Returns variant | None
890886
# - variant: The variant evaluated
891887
# - None: The rule does not apply
892-
lines = ["def a(target_id, attributes):"]
888+
lines = ["def a(attributes):"]
893889
lines += list(f" {line}" for line in (py_common + py_lines))
894890
lines += [" return None"]
895891
lines_str = "\n".join(lines)
@@ -972,15 +968,15 @@ class FlagEvaluation:
972968
"flag",
973969
"variant",
974970
"default",
975-
"target_id",
971+
"attributes",
976972
"rule",
977973
"reason",
978974
"timestamp",
979975
)
980976
flag: str
981977
variant: Variant
982978
default: Variant
983-
target_id: str
979+
attributes: Attributes
984980
rule: str | Literal[""]
985981
reason: Literal["const_rule", "default"]
986982
timestamp: float
@@ -1042,14 +1038,12 @@ def _record_eval_metrics(self, e: FlagEvaluation, dur: float):
10421038
}
10431039
_prom_eval_duration.labels(**labels).observe(dur)
10441040

1045-
def detailed_evaluate_all(self, config: CompiledConfig, names: Iterable[str], id: str, attributes: Attributes = {}) -> dict[str, FlagEvaluation]:
1041+
def detailed_evaluate_all(self, config: CompiledConfig, names: Iterable[str], attributes: Attributes = {}) -> dict[str, FlagEvaluation]:
10461042
"""
10471043
Evaluate all flags for the given id and attributes and return
10481044
FlagEvaluations. detailed_evaluate_all is thread-safe.
10491045
"""
10501046
assert config._feafea_checksum == _feafea_checksum, "incompatible config"
1051-
if not isinstance(id, str):
1052-
raise TypeError(f"id must be a string, not {type(id).__name__}")
10531047
self._validate_attributes_type(attributes)
10541048
merged_attributes = {"__now": time.time(), **attributes}
10551049
now = merged_attributes["__now"]
@@ -1062,7 +1056,8 @@ def detailed_evaluate_all(self, config: CompiledConfig, names: Iterable[str], id
10621056
if flag is None:
10631057
raise ValueError(f"Flag {name} does not exist in the config")
10641058
start = time.perf_counter()
1065-
e = flag.eval(id, merged_attributes)
1059+
e = flag.eval(merged_attributes)
1060+
e.attributes = merged_attributes
10661061
e.timestamp = now
10671062
dur = time.perf_counter() - start
10681063
self._record_eval_metrics(e, dur)
@@ -1088,19 +1083,18 @@ def pop_evaluations(self) -> list[FlagEvaluation]:
10881083
self._evaluations = []
10891084
return es
10901085

1091-
def evaluate(self, config: CompiledConfig, name: str, id: str, attributes: Attributes = {}) -> Variant:
1086+
def evaluate(self, config: CompiledConfig, name: str, attributes: Attributes = {}) -> Variant:
10921087
"""
10931088
Evaluate the given flag. evaluate is thread-safe.
10941089
10951090
name: The name of the flag.
1096-
id: The id of the flag.
10971091
attributes: The attributes to evaluate the flag with.
10981092
"""
1099-
return next(iter(self.detailed_evaluate_all(config, [name], id, attributes).values())).variant
1093+
return next(iter(self.detailed_evaluate_all(config, [name], attributes).values())).variant
11001094

1101-
def evaluate_all(self, config: CompiledConfig, names: Iterable[str], id: str, attributes: Attributes = {}) -> dict[str, Variant]:
1095+
def evaluate_all(self, config: CompiledConfig, names: Iterable[str], attributes: Attributes = {}) -> dict[str, Variant]:
11021096
"""
11031097
Evaluate all flags for the given id and attributes. evaluate_all is
11041098
thread-safe.
11051099
"""
1106-
return {n: e.variant for n, e in self.detailed_evaluate_all(config, names, id, attributes).items()}
1100+
return {n: e.variant for n, e in self.detailed_evaluate_all(config, names, attributes).items()}

tests/test_attribute_only_filters.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_simple(self):
9999
fs = _FilterSet()
100100
fs.parse("f", filter)
101101
c = fs.compile("f", {}, {})
102-
self.assertEqual(c.eval("", attr), expected)
102+
self.assertEqual(c.eval(attr), expected)
103103

104104
valid_syntax_cases = [
105105
"flag:a > 3",
@@ -208,12 +208,12 @@ def test_invalid_attributes(self):
208208
fs = _FilterSet()
209209
fs.parse("f", f"({filter}) and attr:true")
210210
cf = fs.compile("f", {}, {})
211-
self.assertFalse(cf.eval("", {**attrs, "true": True}))
211+
self.assertFalse(cf.eval({**attrs, "true": True}))
212212

213213
fs = _FilterSet()
214214
fs.parse("f", f"({filter}) or attr:true")
215215
cf = fs.compile("f", {}, {})
216-
self.assertTrue(cf.eval("", {**attrs, "true": True}))
216+
self.assertTrue(cf.eval({**attrs, "true": True}))
217217

218218
def test_invalid_symbol_names(self):
219219
cases = [
@@ -287,7 +287,7 @@ def test_inlined_filters(self):
287287
for filter, attr, expected in cases:
288288
with self.subTest(f"{filter}, {attr}"):
289289
c = fs.compile(filter, {}, {})
290-
self.assertEqual(c.eval("", attr), expected)
290+
self.assertEqual(c.eval(attr), expected)
291291

292292
def test_missing_ref_filter(self):
293293
fs = _FilterSet()
@@ -314,11 +314,11 @@ def test_insplit_function_distribution_and_exclusivity(self):
314314

315315
variant_count = [0, 0, 0]
316316
for id in range(100000):
317-
if filter_a.eval("", {"a": id}):
317+
if filter_a.eval({"a": id}):
318318
variant_count[0] += 1
319-
if filter_b.eval("", {"a": id}):
319+
if filter_b.eval({"a": id}):
320320
variant_count[1] += 1
321-
if filter_c.eval("", {"a": id}):
321+
if filter_c.eval({"a": id}):
322322
variant_count[2] += 1
323323

324324
self.assertEqual(sum(variant_count), 100000)
@@ -338,7 +338,7 @@ def test_insplit_function_seed_spread(self):
338338
# Here, we keep the attribute value constant and vary the seed and
339339
# we expect the same distribution as if we were varying the
340340
# attribute value.
341-
if filter.eval("", {"a": "constant", "__seed": id}):
341+
if filter.eval({"a": "constant", "__seed": id}):
342342
true_count += 1
343343
else:
344344
false_count += 1

tests/test_attribute_only_filters_undefined_refs.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def test_simple(self):
101101
fs = _FilterSet()
102102
fs.parse("f", filter)
103103
c = fs.compile("f", {}, {})
104-
self.assertEqual(c.eval("", attr), expected)
104+
self.assertEqual(c.eval(attr), expected)
105105

106106
valid_syntax_cases = [
107107
"flag:a > 3",
@@ -182,12 +182,12 @@ def test_invalid_attributes(self):
182182
fs = _FilterSet()
183183
fs.parse("f", f"({filter}) and attr:true")
184184
cf = fs.compile("f", {}, {})
185-
self.assertFalse(cf.eval("", {**attrs, "true": True}))
185+
self.assertFalse(cf.eval({**attrs, "true": True}))
186186

187187
fs = _FilterSet()
188188
fs.parse("f", f"({filter}) or attr:true")
189189
cf = fs.compile("f", {}, {})
190-
self.assertTrue(cf.eval("", {**attrs, "true": True}))
190+
self.assertTrue(cf.eval({**attrs, "true": True}))
191191

192192
def test_invalid_symbol_names(self):
193193
cases = [
@@ -261,7 +261,7 @@ def test_inlined_filters(self):
261261
for filter, attr, expected in cases:
262262
with self.subTest(f"{filter}, {attr}"):
263263
c = fs.compile(filter, {}, {})
264-
self.assertEqual(c.eval("", attr), expected)
264+
self.assertEqual(c.eval(attr), expected)
265265

266266
def test_missing_ref_filter(self):
267267
fs = _FilterSet()
@@ -270,11 +270,11 @@ def test_missing_ref_filter(self):
270270
fs.parse("d", "filter:c or filter:e")
271271
fs.parse("e", "attr:age > 10")
272272
c = fs.compile("a", {}, {})
273-
self.assertFalse(c.eval("", {}))
273+
self.assertFalse(c.eval({}))
274274
c = fs.compile("c", {}, {})
275-
self.assertFalse(c.eval("", {}))
275+
self.assertFalse(c.eval({}))
276276
c = fs.compile("e", {}, {})
277-
self.assertTrue(c.eval("", {"age": 20}))
277+
self.assertTrue(c.eval({"age": 20}))
278278

279279
def test_rule_and_flag_refs(self):
280280
fs = _FilterSet()
@@ -295,11 +295,11 @@ def test_insplit_function_distribution_and_exclusivity(self):
295295

296296
variant_count = [0, 0, 0]
297297
for id in range(100000):
298-
if filter_a.eval("", {"a": id}):
298+
if filter_a.eval({"a": id}):
299299
variant_count[0] += 1
300-
if filter_b.eval("", {"a": id}):
300+
if filter_b.eval({"a": id}):
301301
variant_count[1] += 1
302-
if filter_c.eval("", {"a": id}):
302+
if filter_c.eval({"a": id}):
303303
variant_count[2] += 1
304304

305305
self.assertEqual(sum(variant_count), 100000)
@@ -319,7 +319,7 @@ def test_insplit_function_seed_spread(self):
319319
# Here, we keep the attribute value constant and vary the seed and
320320
# we expect the same distribution as if we were varying the
321321
# attribute value.
322-
if filter.eval("", {"a": "constant", "__seed": id}):
322+
if filter.eval({"a": "constant", "__seed": id}):
323323
true_count += 1
324324
else:
325325
false_count += 1
@@ -346,7 +346,7 @@ def test_undefined_flag_reference_in_pythonize(self):
346346
fs = _FilterSet()
347347
fs.parse("f", filter_str)
348348
c = fs.compile("f", {}, {}) # Empty flags dict means undefined
349-
self.assertEqual(c.eval("", attr), expected)
349+
self.assertEqual(c.eval(attr), expected)
350350

351351
def test_undefined_rule_reference_in_pythonize(self):
352352
"""Test that undefined rule references return False when ignore_undefined_refs is True."""
@@ -362,4 +362,4 @@ def test_undefined_rule_reference_in_pythonize(self):
362362
fs = _FilterSet()
363363
fs.parse("f", filter_str)
364364
c = fs.compile("f", {}, {}) # Empty rules dict means undefined
365-
self.assertEqual(c.eval("", attr), expected)
365+
self.assertEqual(c.eval(attr), expected)

tests/test_configs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ def test_config_serialization(self):
207207
b = self._valid_config.to_bytes()
208208
cc = CompiledConfig.from_bytes(b)
209209
self.assertSetEqual(set(self._valid_config.flags), set(cc.flags))
210-
self.assertEqual(self._valid_config.flags["a"].eval("", {}).variant, cc.flags["a"].eval("", {}).variant)
210+
self.assertEqual(self._valid_config.flags["a"].eval({}).variant, cc.flags["a"].eval({}).variant)
211211

212212
def test_valid_const_config(self):
213213
cases = [
@@ -233,7 +233,7 @@ def test_valid_const_config(self):
233233

234234
for flag, attrs, reason, rule, variant in cases:
235235
with self.subTest(f"flag={flag}, attrs={attrs}"):
236-
ev = self._valid_config.flags[flag].eval("", attrs)
236+
ev = self._valid_config.flags[flag].eval(attrs)
237237
self.assertEqual(ev.variant, variant)
238238
self.assertEqual(ev.reason, reason)
239239
self.assertEqual(ev.rule, rule)

tests/test_configs_undefined_refs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def test_config_serialization(self):
152152
b = self._valid_config.to_bytes()
153153
cc = CompiledConfig.from_bytes(b)
154154
self.assertSetEqual(set(self._valid_config.flags), set(cc.flags))
155-
self.assertEqual(self._valid_config.flags["a"].eval("", {}).variant, cc.flags["a"].eval("", {}).variant)
155+
self.assertEqual(self._valid_config.flags["a"].eval({}).variant, cc.flags["a"].eval({}).variant)
156156

157157
def test_valid_const_config(self):
158158
cases = [
@@ -178,7 +178,7 @@ def test_valid_const_config(self):
178178

179179
for flag, attrs, reason, rule, variant in cases:
180180
with self.subTest(f"flag={flag}, attrs={attrs}"):
181-
ev = self._valid_config.flags[flag].eval("", attrs)
181+
ev = self._valid_config.flags[flag].eval(attrs)
182182
self.assertEqual(ev.variant, variant)
183183
self.assertEqual(ev.reason, reason)
184184
self.assertEqual(ev.rule, rule)

0 commit comments

Comments
 (0)