Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/cranky-duchess-ilmatar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/openfeature-provider-posthog: patch
---

Support hexadecimal numeric variants in the OpenFeature provider.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import logging
import re
from typing import Any, Callable, Mapping, Optional, Sequence, TypeVar, Union

from openfeature.evaluation_context import EvaluationContext
Expand Down Expand Up @@ -53,7 +54,9 @@ class PostHogProvider(AbstractProvider):
Flag-type mapping (all via ``get_feature_flag_result``):
* boolean -> ``enabled``
* string -> the multivariate ``variant`` key
* int/float -> the ``variant`` parsed to a number
* int/float -> the ``variant`` parsed to a number, including unsigned
hexadecimal integers such as ``0x10`` (returned as int or float,
respectively)
* object -> the flag's JSON ``payload``

Args:
Expand Down Expand Up @@ -202,7 +205,7 @@ def _resolve_number(
flag_key: str,
default_value: _N,
evaluation_context: Optional[EvaluationContext],
ctor: Callable[[str], _N],
ctor: Callable[[Union[str, int]], _N],
) -> FlagResolutionDetails[_N]:
result = self._resolve(flag_key, evaluation_context)
if result.variant is None:
Expand All @@ -213,8 +216,13 @@ def _resolve_number(
f"Flag '{flag_key}' has no variant to parse as {ctor.__name__}."
)
try:
value = ctor(result.variant)
except (TypeError, ValueError) as exc:
variant: Union[str, int] = result.variant
if isinstance(variant, str) and re.fullmatch(
r"0[xX][0-9a-fA-F]+", variant.strip()
):
variant = int(variant, 16)
value = ctor(variant)
except (TypeError, ValueError, OverflowError) as exc:
raise TypeMismatchError(
f"Flag '{flag_key}' variant '{result.variant}' is not a valid "
f"{ctor.__name__}."
Expand Down
61 changes: 61 additions & 0 deletions openfeature-provider/tests/test_provider_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ def test_string_on_boolean_flag_is_type_mismatch(fake_client):
("resolve_integer_details", "3", 3),
("resolve_float_details", "3.5", 3.5),
("resolve_float_details", "3", 3.0),
("resolve_integer_details", 42, 42),
("resolve_integer_details", 3.5, 3),
("resolve_float_details", 42, 42.0),
("resolve_float_details", 3.5, 3.5),
],
)
def test_number_variant_parse(fake_client, resolver, variant, expected):
Expand Down Expand Up @@ -100,6 +104,63 @@ def test_number_variant_parse_failure(fake_client, resolver, variant):
getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))


@pytest.mark.parametrize(
"resolver", ["resolve_integer_details", "resolve_float_details"]
)
@pytest.mark.parametrize(
("variant", "expected"),
[("0x10", 16), ("0Xff", 255), (" 0xA0 ", 160), ("0x0", 0), ("042", 42)],
)
def test_hex_number_variant(fake_client, resolver, variant, expected):
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
details = getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))
assert details.value == expected
assert type(details.value) is (
int if resolver == "resolve_integer_details" else float
)
assert details.variant == variant


@pytest.mark.parametrize(
"resolver", ["resolve_integer_details", "resolve_float_details"]
)
@pytest.mark.parametrize(
"variant", ["0x", "0xgg", "0x1.5", "0x_10", "0x1_0", "-0x10", "+0x10"]
)
def test_invalid_hex_number_variant(fake_client, resolver, variant):
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
with pytest.raises(TypeMismatchError):
getattr(_provider(fake_client), resolver)("n", 0, EvaluationContext("u"))


def test_hex_integer_beyond_decimal_string_limit(fake_client):
variant = "0x" + "f" * 4000
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
details = _provider(fake_client).resolve_integer_details(
"n", 0, EvaluationContext("u")
)
assert details.value == (1 << 16000) - 1
assert details.variant == variant


def test_large_representable_hex_float(fake_client):
variant = "0x" + "f" * 255
fake_client.get_feature_flag_result.return_value = make_result(variant=variant)
details = _provider(fake_client).resolve_float_details(
"n", 0.0, EvaluationContext("u")
)
assert details.value == float(int(variant, 16))
assert isinstance(details.value, float)


def test_hex_float_overflow_is_type_mismatch(fake_client):
fake_client.get_feature_flag_result.return_value = make_result(
variant="0x" + "f" * 4000
)
with pytest.raises(TypeMismatchError):
_provider(fake_client).resolve_float_details("n", 0.0, EvaluationContext("u"))


def test_object_payload(fake_client):
fake_client.get_feature_flag_result.return_value = make_result(
enabled=True, variant="v1", payload={"color": "blue"}
Expand Down
Loading