From 8d0f04e46f5070b36bdd890ab488e67c02a60290 Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Sat, 21 Feb 2026 01:51:40 +1100 Subject: [PATCH 1/4] fix: support flat traits Signed-off-by: Danju Visvanathan --- openfeature_flagsmith/provider.py | 9 ++- tests/test_provider.py | 106 ++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/openfeature_flagsmith/provider.py b/openfeature_flagsmith/provider.py index a9a6661..5d9bb67 100644 --- a/openfeature_flagsmith/provider.py +++ b/openfeature_flagsmith/provider.py @@ -134,8 +134,15 @@ def _resolve( def _get_flags(self, evaluation_context: EvaluationContext = EvaluationContext()): if targeting_key := evaluation_context.targeting_key: + traits = {} + for key, value in evaluation_context.attributes.items(): + if key == "traits": + continue + else: + traits[key] = value + traits.update(evaluation_context.attributes.get("traits", {})) return self._client.get_identity_flags( identifier=targeting_key, - traits=evaluation_context.attributes.get("traits", {}), + traits=traits, ) return self._client.get_environment_flags() diff --git a/tests/test_provider.py b/tests/test_provider.py index 1126482..edd77b7 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -319,6 +319,112 @@ def test_identity_flags_are_used_if_targeting_key_provided( ) +def test_identity_flags_are_used_with_flat_attributes( + mock_flagsmith_client: MagicMock, +) -> None: + # Given + key = "key" + targeting_key = "targeting_key" + traits = {"foo": "bar", "age": 25} + value = "foo" + default_value = "default" + + provider = FlagsmithProvider(mock_flagsmith_client) + + mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError() + mock_flagsmith_client.get_identity_flags.return_value = Flags( + {key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)} + ) + + # When + result = provider.resolve_string_details( + flag_key=key, + default_value=default_value, + evaluation_context=EvaluationContext( + targeting_key=targeting_key, attributes=traits + ), + ) + + # Then + assert result.value == value + assert result.error_code is None + assert result.reason is None + + mock_flagsmith_client.get_identity_flags.assert_called_once_with( + identifier=targeting_key, traits=traits + ) + + +def test_identity_flags_flat_attributes_and_nested_traits_are_merged( + mock_flagsmith_client: MagicMock, +) -> None: + # Given + key = "key" + targeting_key = "targeting_key" + value = "foo" + default_value = "default" + + provider = FlagsmithProvider(mock_flagsmith_client) + + mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError() + mock_flagsmith_client.get_identity_flags.return_value = Flags( + {key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)} + ) + + # When + result = provider.resolve_string_details( + flag_key=key, + default_value=default_value, + evaluation_context=EvaluationContext( + targeting_key=targeting_key, + attributes={"flat_trait": "flat_value", "traits": {"nested_trait": "nested_value"}}, + ), + ) + + # Then + assert result.value == value + assert result.error_code is None + assert result.reason is None + + mock_flagsmith_client.get_identity_flags.assert_called_once_with( + identifier=targeting_key, + traits={"flat_trait": "flat_value", "nested_trait": "nested_value"}, + ) + + +def test_identity_flags_nested_traits_take_precedence_over_flat_attributes( + mock_flagsmith_client: MagicMock, +) -> None: + # Given + key = "key" + targeting_key = "targeting_key" + value = "foo" + default_value = "default" + + provider = FlagsmithProvider(mock_flagsmith_client) + + mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError() + mock_flagsmith_client.get_identity_flags.return_value = Flags( + {key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)} + ) + + # When + provider.resolve_string_details( + flag_key=key, + default_value=default_value, + evaluation_context=EvaluationContext( + targeting_key=targeting_key, + attributes={"shared_key": "flat_value", "traits": {"shared_key": "nested_value"}}, + ), + ) + + # Then + mock_flagsmith_client.get_identity_flags.assert_called_once_with( + identifier=targeting_key, + traits={"shared_key": "nested_value"}, + ) + + def test_resolve_boolean_details_uses_enabled_when_use_boolean_config_value_is_false( mock_flagsmith_client: MagicMock, ) -> None: From 9a7468f181186b985b181183939ee6e2ef397f3a Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Tue, 24 Feb 2026 11:49:48 +1100 Subject: [PATCH 2/4] chore: run formatting Signed-off-by: Danju Visvanathan --- tests/test_provider.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_provider.py b/tests/test_provider.py index edd77b7..75d9be3 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -377,7 +377,10 @@ def test_identity_flags_flat_attributes_and_nested_traits_are_merged( default_value=default_value, evaluation_context=EvaluationContext( targeting_key=targeting_key, - attributes={"flat_trait": "flat_value", "traits": {"nested_trait": "nested_value"}}, + attributes={ + "flat_trait": "flat_value", + "traits": {"nested_trait": "nested_value"}, + }, ), ) @@ -414,7 +417,10 @@ def test_identity_flags_nested_traits_take_precedence_over_flat_attributes( default_value=default_value, evaluation_context=EvaluationContext( targeting_key=targeting_key, - attributes={"shared_key": "flat_value", "traits": {"shared_key": "nested_value"}}, + attributes={ + "shared_key": "flat_value", + "traits": {"shared_key": "nested_value"}, + }, ), ) From ee3caba6784073e7a401a849135d333fc5da0d46 Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Wed, 25 Feb 2026 21:50:46 +1100 Subject: [PATCH 3/4] fix: refactor attributes building Signed-off-by: Danju Visvanathan --- openfeature_flagsmith/provider.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openfeature_flagsmith/provider.py b/openfeature_flagsmith/provider.py index 5d9bb67..29f0a10 100644 --- a/openfeature_flagsmith/provider.py +++ b/openfeature_flagsmith/provider.py @@ -134,15 +134,10 @@ def _resolve( def _get_flags(self, evaluation_context: EvaluationContext = EvaluationContext()): if targeting_key := evaluation_context.targeting_key: - traits = {} - for key, value in evaluation_context.attributes.items(): - if key == "traits": - continue - else: - traits[key] = value - traits.update(evaluation_context.attributes.get("traits", {})) + nested_traits = evaluation_context.attributes.pop("traits", {}) + flattened_traits = {**evaluation_context.attributes, **nested_traits} return self._client.get_identity_flags( identifier=targeting_key, - traits=traits, + traits=flattened_traits, ) return self._client.get_environment_flags() From f43490ffe951bae8f381b9330c4201e7eb35f9cd Mon Sep 17 00:00:00 2001 From: Danju Visvanathan Date: Fri, 27 Feb 2026 02:39:36 +1100 Subject: [PATCH 4/4] chore: add example to README.md --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8689fa4..30e6459 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,26 @@ provider = FlagsmithProvider( ) ``` -The provider can then be used with the OpenFeature client as per +The provider can then be used with the OpenFeature client as per [the documentation](https://openfeature.dev/docs/reference/concepts/evaluation-api#setting-a-provider). +### Evaluation Context + +The evaluation context supports traits in two ways: +1. Flat top-level attributes +2. A nested traits object + +The two forms are merged and sent to Flagsmith, with the traits object taking precedence if keys conflict. + +```python +context = EvaluationContext( # Traits are: {"abc":"def", "foo": "bar2"} + targeting_key="user", + attributes={ + "foo": "bar", + "abc": "def", + "traits": {"foo": "bar2"} + }, +) + +``` +