Skip to content

Commit ba2ac86

Browse files
authored
fix: support hexadecimal OpenFeature numeric variants (#980)
* fix: support hexadecimal OpenFeature numeric variants * fix: preserve numeric variants and parse large hex integers directly
1 parent f5cbdef commit ba2ac86

3 files changed

Lines changed: 78 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/openfeature-provider-posthog: patch
3+
---
4+
5+
Support hexadecimal numeric variants in the OpenFeature provider.

‎openfeature-provider/openfeature/contrib/provider/posthog/provider.py‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import logging
11+
import re
1112
from typing import Any, Callable, Mapping, Optional, Sequence, TypeVar, Union
1213

1314
from openfeature.evaluation_context import EvaluationContext
@@ -53,7 +54,9 @@ class PostHogProvider(AbstractProvider):
5354
Flag-type mapping (all via ``get_feature_flag_result``):
5455
* boolean -> ``enabled``
5556
* string -> the multivariate ``variant`` key
56-
* int/float -> the ``variant`` parsed to a number
57+
* int/float -> the ``variant`` parsed to a number, including unsigned
58+
hexadecimal integers such as ``0x10`` (returned as int or float,
59+
respectively)
5760
* object -> the flag's JSON ``payload``
5861
5962
Args:
@@ -202,7 +205,7 @@ def _resolve_number(
202205
flag_key: str,
203206
default_value: _N,
204207
evaluation_context: Optional[EvaluationContext],
205-
ctor: Callable[[str], _N],
208+
ctor: Callable[[Union[str, int]], _N],
206209
) -> FlagResolutionDetails[_N]:
207210
result = self._resolve(flag_key, evaluation_context)
208211
if result.variant is None:
@@ -213,8 +216,13 @@ def _resolve_number(
213216
f"Flag '{flag_key}' has no variant to parse as {ctor.__name__}."
214217
)
215218
try:
216-
value = ctor(result.variant)
217-
except (TypeError, ValueError) as exc:
219+
variant: Union[str, int] = result.variant
220+
if isinstance(variant, str) and re.fullmatch(
221+
r"0[xX][0-9a-fA-F]+", variant.strip()
222+
):
223+
variant = int(variant, 16)
224+
value = ctor(variant)
225+
except (TypeError, ValueError, OverflowError) as exc:
218226
raise TypeMismatchError(
219227
f"Flag '{flag_key}' variant '{result.variant}' is not a valid "
220228
f"{ctor.__name__}."

‎openfeature-provider/tests/test_provider_unit.py‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ def test_string_on_boolean_flag_is_type_mismatch(fake_client):
7373
("resolve_integer_details", "3", 3),
7474
("resolve_float_details", "3.5", 3.5),
7575
("resolve_float_details", "3", 3.0),
76+
("resolve_integer_details", 42, 42),
77+
("resolve_integer_details", 3.5, 3),
78+
("resolve_float_details", 42, 42.0),
79+
("resolve_float_details", 3.5, 3.5),
7680
],
7781
)
7882
def test_number_variant_parse(fake_client, resolver, variant, expected):
@@ -100,6 +104,63 @@ def test_number_variant_parse_failure(fake_client, resolver, variant):
100104
getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))
101105

102106

107+
@pytest.mark.parametrize(
108+
"resolver", ["resolve_integer_details", "resolve_float_details"]
109+
)
110+
@pytest.mark.parametrize(
111+
("variant", "expected"),
112+
[("0x10", 16), ("0Xff", 255), (" 0xA0 ", 160), ("0x0", 0), ("042", 42)],
113+
)
114+
def test_hex_number_variant(fake_client, resolver, variant, expected):
115+
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
116+
details = getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))
117+
assert details.value == expected
118+
assert type(details.value) is (
119+
int if resolver == "resolve_integer_details" else float
120+
)
121+
assert details.variant == variant
122+
123+
124+
@pytest.mark.parametrize(
125+
"resolver", ["resolve_integer_details", "resolve_float_details"]
126+
)
127+
@pytest.mark.parametrize(
128+
"variant", ["0x", "0xgg", "0x1.5", "0x_10", "0x1_0", "-0x10", "+0x10"]
129+
)
130+
def test_invalid_hex_number_variant(fake_client, resolver, variant):
131+
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
132+
with pytest.raises(TypeMismatchError):
133+
getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))
134+
135+
136+
def test_hex_integer_beyond_decimal_string_limit(fake_client):
137+
variant = "0x" + "f" * 4000
138+
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
139+
details = _provider(fake_client).resolve_integer_details(
140+
"n", 0, EvaluationContext("u")
141+
)
142+
assert details.value == (1 << 16000) - 1
143+
assert details.variant == variant
144+
145+
146+
def test_large_representable_hex_float(fake_client):
147+
variant = "0x" + "f" * 255
148+
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
149+
details = _provider(fake_client).resolve_float_details(
150+
"n", 0.0, EvaluationContext("u")
151+
)
152+
assert details.value == float(int(variant, 16))
153+
assert isinstance(details.value, float)
154+
155+
156+
def test_hex_float_overflow_is_type_mismatch(fake_client):
157+
fake_client.get_feature_flag_result.return_value = make_result(
158+
variant="0x" + "f" * 4000
159+
)
160+
with pytest.raises(TypeMismatchError):
161+
_provider(fake_client).resolve_float_details("n", 0.0, EvaluationContext("u"))
162+
163+
103164
def test_object_payload(fake_client):
104165
fake_client.get_feature_flag_result.return_value = make_result(
105166
enabled=True, variant="v1", payload={"color": "blue"}

0 commit comments

Comments
 (0)