From 6c4417467ac957852424f92b86912a852872f219 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:30:12 +0000 Subject: [PATCH 1/9] Initial plan for issue From b00f2889544cce1dd169285a1107ecdd822c45de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:38:28 +0000 Subject: [PATCH 2/9] Implement last feature flag wins behavior for duplicate IDs Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- featuremanagement/_featuremanagerbase.py | 20 +++- tests/test_feature_manager.py | 126 +++++++++++++++++++++++ tests/test_feature_manager_async.py | 72 +++++++++++++ 3 files changed, 213 insertions(+), 5 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 845db2c..9839835 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -27,6 +27,7 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]: """ Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object. + If multiple feature flags have the same id, the last one wins. :param Mapping configuration: Configuration object. :param str feature_flag_name: Name of the feature flag. @@ -40,21 +41,24 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) if not feature_flags or not isinstance(feature_flags, list): return None + last_match = None for feature_flag in feature_flags: if feature_flag.get("id") == feature_flag_name: - return FeatureFlag.convert_from_json(feature_flag) + last_match = feature_flag + + if last_match: + return FeatureFlag.convert_from_json(last_match) return None def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: """ - List of all feature flag names. + List of all feature flag names. If there are duplicate names, only unique names are returned. :param Mapping configuration: Configuration object. :return: List of feature flag names. """ - feature_flag_names = [] feature_management = configuration.get(FEATURE_MANAGEMENT_KEY) if not feature_management or not isinstance(feature_management, Mapping): return [] @@ -62,10 +66,16 @@ def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: if not feature_flags or not isinstance(feature_flags, list): return [] + # Use a set to track unique names and a list to preserve order + seen = set() + unique_names = [] for feature_flag in feature_flags: - feature_flag_names.append(feature_flag.get("id")) + flag_id = feature_flag.get("id") + if flag_id not in seen: + seen.add(flag_id) + unique_names.append(flag_id) - return feature_flag_names + return unique_names class FeatureManagerBase(ABC): diff --git a/tests/test_feature_manager.py b/tests/test_feature_manager.py index 7813835..e1bafc7 100644 --- a/tests/test_feature_manager.py +++ b/tests/test_feature_manager.py @@ -150,6 +150,132 @@ def fake_telemetry_callback(self, evaluation_event): assert evaluation_event self.called_telemetry = True + # method: duplicate_feature_flag_handling + def test_duplicate_feature_flags_last_wins(self): + """Test that when multiple feature flags have the same ID, the last one wins.""" + feature_flags = { + "feature_management": { + "feature_flags": [ + { + "id": "DuplicateFlag", + "description": "First", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Second", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Third", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + ] + } + } + feature_manager = FeatureManager(feature_flags) + + # The last flag should win (enabled: true) + assert feature_manager.is_enabled("DuplicateFlag") == True + + # Should only list unique names + flag_names = feature_manager.list_feature_flag_names() + assert "DuplicateFlag" in flag_names + # Count how many times DuplicateFlag appears in the list + duplicate_count = flag_names.count("DuplicateFlag") + assert duplicate_count == 1, f"Expected DuplicateFlag to appear once, but appeared {duplicate_count} times" + + def test_duplicate_feature_flags_last_wins_disabled(self): + """Test that when multiple feature flags have the same ID, the last one wins even if disabled.""" + feature_flags = { + "feature_management": { + "feature_flags": [ + { + "id": "DuplicateFlag", + "description": "First", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Second", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Third", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + ] + } + } + feature_manager = FeatureManager(feature_flags) + + # The last flag should win (enabled: false) + assert feature_manager.is_enabled("DuplicateFlag") == False + + def test_duplicate_feature_flags_mixed_with_unique(self): + """Test behavior with a mix of duplicate and unique feature flags.""" + feature_flags = { + "feature_management": { + "feature_flags": [ + { + "id": "UniqueFlag1", + "description": "First unique", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "First duplicate", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + { + "id": "UniqueFlag2", + "description": "Second unique", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Second duplicate", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "UniqueFlag3", + "description": "Third unique", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + ] + } + } + feature_manager = FeatureManager(feature_flags) + + # Test unique flags work as expected + assert feature_manager.is_enabled("UniqueFlag1") == True + assert feature_manager.is_enabled("UniqueFlag2") == False + assert feature_manager.is_enabled("UniqueFlag3") == True + + # Test duplicate flag - last should win (enabled: true) + assert feature_manager.is_enabled("DuplicateFlag") == True + + # Test list includes all unique names + flag_names = feature_manager.list_feature_flag_names() + expected_names = ["UniqueFlag1", "DuplicateFlag", "UniqueFlag2", "UniqueFlag3"] + assert set(flag_names) == set(expected_names) + # Ensure each name appears only once + for name in expected_names: + assert flag_names.count(name) == 1 + class AlwaysOn(FeatureFilter): def evaluate(self, context, **kwargs): diff --git a/tests/test_feature_manager_async.py b/tests/test_feature_manager_async.py index 1f6c959..e14bae9 100644 --- a/tests/test_feature_manager_async.py +++ b/tests/test_feature_manager_async.py @@ -179,6 +179,78 @@ async def fake_telemetry_callback_async(self, evaluation_event): assert evaluation_event self.called_telemetry = True + # method: duplicate_feature_flag_handling + @pytest.mark.asyncio + async def test_duplicate_feature_flags_last_wins_async(self): + """Test that when multiple feature flags have the same ID, the last one wins.""" + feature_flags = { + "feature_management": { + "feature_flags": [ + { + "id": "DuplicateFlag", + "description": "First", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Second", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Third", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + ] + } + } + feature_manager = FeatureManager(feature_flags) + + # The last flag should win (enabled: true) + assert await feature_manager.is_enabled("DuplicateFlag") == True + + # Should only list unique names + flag_names = feature_manager.list_feature_flag_names() + assert "DuplicateFlag" in flag_names + # Count how many times DuplicateFlag appears in the list + duplicate_count = flag_names.count("DuplicateFlag") + assert duplicate_count == 1, f"Expected DuplicateFlag to appear once, but appeared {duplicate_count} times" + + @pytest.mark.asyncio + async def test_duplicate_feature_flags_last_wins_disabled_async(self): + """Test that when multiple feature flags have the same ID, the last one wins even if disabled.""" + feature_flags = { + "feature_management": { + "feature_flags": [ + { + "id": "DuplicateFlag", + "description": "First", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Second", + "enabled": "true", + "conditions": {"client_filters": []}, + }, + { + "id": "DuplicateFlag", + "description": "Third", + "enabled": "false", + "conditions": {"client_filters": []}, + }, + ] + } + } + feature_manager = FeatureManager(feature_flags) + + # The last flag should win (enabled: false) + assert await feature_manager.is_enabled("DuplicateFlag") == False + class AlwaysOn(FeatureFilter): async def evaluate(self, context, **kwargs): From b964759691cd758e3655a5aa2bd430b524c252df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 18:50:52 +0000 Subject: [PATCH 3/9] Optimize _get_feature_flag to iterate backwards for better performance Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- featuremanagement/_featuremanagerbase.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 9839835..15b32a5 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -41,13 +41,10 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) if not feature_flags or not isinstance(feature_flags, list): return None - last_match = None - for feature_flag in feature_flags: + # Iterate backwards to find the last matching feature flag more efficiently + for feature_flag in reversed(feature_flags): if feature_flag.get("id") == feature_flag_name: - last_match = feature_flag - - if last_match: - return FeatureFlag.convert_from_json(last_match) + return FeatureFlag.convert_from_json(feature_flag) return None From e9e84e1e3ec842783ef7582b3fe6740e8c7a6cfe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:00:03 +0000 Subject: [PATCH 4/9] Optimize performance by caching reversed feature flags and simplify unique list generation Co-authored-by: mrm9084 <1054559+mrm9084@users.noreply.github.com> --- featuremanagement/_featuremanagerbase.py | 43 +++++++++++++++--------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 15b32a5..0ab46fb 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -24,13 +24,14 @@ logger = logging.getLogger(__name__) -def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]: +def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str, reversed_feature_flags: Optional[List] = None) -> Optional[FeatureFlag]: """ Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object. If multiple feature flags have the same id, the last one wins. :param Mapping configuration: Configuration object. :param str feature_flag_name: Name of the feature flag. + :param List reversed_feature_flags: Optional pre-reversed list of feature flags for performance. :return: FeatureFlag :rtype: FeatureFlag """ @@ -41,8 +42,10 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) if not feature_flags or not isinstance(feature_flags, list): return None - # Iterate backwards to find the last matching feature flag more efficiently - for feature_flag in reversed(feature_flags): + # Use pre-reversed list if available, otherwise reverse on demand + flags_to_iterate = reversed_feature_flags if reversed_feature_flags is not None else reversed(feature_flags) + + for feature_flag in flags_to_iterate: if feature_flag.get("id") == feature_flag_name: return FeatureFlag.convert_from_json(feature_flag) @@ -51,7 +54,8 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: """ - List of all feature flag names. If there are duplicate names, only unique names are returned. + List of all feature flag names. If there are duplicate names, only unique names are returned + in order of first appearance. :param Mapping configuration: Configuration object. :return: List of feature flag names. @@ -63,16 +67,9 @@ def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: if not feature_flags or not isinstance(feature_flags, list): return [] - # Use a set to track unique names and a list to preserve order - seen = set() - unique_names = [] - for feature_flag in feature_flags: - flag_id = feature_flag.get("id") - if flag_id not in seen: - seen.add(flag_id) - unique_names.append(flag_id) - - return unique_names + # Use dict.fromkeys() to preserve order while ensuring uniqueness + flag_ids = [feature_flag.get("id") for feature_flag in feature_flags] + return list(dict.fromkeys(flag_ids)) class FeatureManagerBase(ABC): @@ -86,10 +83,25 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Any): self._configuration = configuration self._cache: Dict[str, Optional[FeatureFlag]] = {} self._copy = configuration.get(FEATURE_MANAGEMENT_KEY) + self._reversed_feature_flags: Optional[List] = None self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None) self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop( "targeting_context_accessor", None ) + self._update_reversed_feature_flags() + + def _update_reversed_feature_flags(self) -> None: + """Update the cached reversed feature flags list when configuration changes.""" + feature_management = self._configuration.get(FEATURE_MANAGEMENT_KEY) + if not feature_management or not isinstance(feature_management, Mapping): + self._reversed_feature_flags = None + return + feature_flags = feature_management.get(FEATURE_FLAG_KEY) + if not feature_flags or not isinstance(feature_flags, list): + self._reversed_feature_flags = None + return + + self._reversed_feature_flags = list(reversed(feature_flags)) @staticmethod def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None: @@ -277,9 +289,10 @@ def _check_feature_base(self, feature_flag_id: str) -> Tuple[EvaluationEvent, bo if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY): self._cache = {} self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY) + self._update_reversed_feature_flags() if not self._cache.get(feature_flag_id): - feature_flag = _get_feature_flag(self._configuration, feature_flag_id) + feature_flag = _get_feature_flag(self._configuration, feature_flag_id, self._reversed_feature_flags) self._cache[feature_flag_id] = feature_flag else: feature_flag = self._cache.get(feature_flag_id) From 71c31838642b3fa62a3a325bb0fbde0eb14fdd7c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 19 Jun 2025 12:53:02 -0700 Subject: [PATCH 5/9] fix reverse search --- featuremanagement/_featuremanagerbase.py | 37 ++++++------------------ 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 0ab46fb..c669b4f 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -24,14 +24,13 @@ logger = logging.getLogger(__name__) -def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str, reversed_feature_flags: Optional[List] = None) -> Optional[FeatureFlag]: +def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) -> Optional[FeatureFlag]: """ Gets the FeatureFlag json from the configuration, if it exists it gets converted to a FeatureFlag object. If multiple feature flags have the same id, the last one wins. :param Mapping configuration: Configuration object. :param str feature_flag_name: Name of the feature flag. - :param List reversed_feature_flags: Optional pre-reversed list of feature flags for performance. :return: FeatureFlag :rtype: FeatureFlag """ @@ -41,21 +40,20 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str, feature_flags = feature_management.get(FEATURE_FLAG_KEY) if not feature_flags or not isinstance(feature_flags, list): return None - - # Use pre-reversed list if available, otherwise reverse on demand - flags_to_iterate = reversed_feature_flags if reversed_feature_flags is not None else reversed(feature_flags) - for feature_flag in flags_to_iterate: - if feature_flag.get("id") == feature_flag_name: - return FeatureFlag.convert_from_json(feature_flag) + index = len(feature_flags) - 1 + + while index >= 0: + if feature_flags[index].get("id") == feature_flag_name: + return FeatureFlag.convert_from_json(feature_flags[index]) + index -= 1 return None def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: """ - List of all feature flag names. If there are duplicate names, only unique names are returned - in order of first appearance. + List of feature flag names, with duplicates removed. :param Mapping configuration: Configuration object. :return: List of feature flag names. @@ -67,7 +65,6 @@ def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: if not feature_flags or not isinstance(feature_flags, list): return [] - # Use dict.fromkeys() to preserve order while ensuring uniqueness flag_ids = [feature_flag.get("id") for feature_flag in feature_flags] return list(dict.fromkeys(flag_ids)) @@ -83,25 +80,10 @@ def __init__(self, configuration: Mapping[str, Any], **kwargs: Any): self._configuration = configuration self._cache: Dict[str, Optional[FeatureFlag]] = {} self._copy = configuration.get(FEATURE_MANAGEMENT_KEY) - self._reversed_feature_flags: Optional[List] = None self._on_feature_evaluated = kwargs.pop("on_feature_evaluated", None) self._targeting_context_accessor: Optional[Callable[[], TargetingContext]] = kwargs.pop( "targeting_context_accessor", None ) - self._update_reversed_feature_flags() - - def _update_reversed_feature_flags(self) -> None: - """Update the cached reversed feature flags list when configuration changes.""" - feature_management = self._configuration.get(FEATURE_MANAGEMENT_KEY) - if not feature_management or not isinstance(feature_management, Mapping): - self._reversed_feature_flags = None - return - feature_flags = feature_management.get(FEATURE_FLAG_KEY) - if not feature_flags or not isinstance(feature_flags, list): - self._reversed_feature_flags = None - return - - self._reversed_feature_flags = list(reversed(feature_flags)) @staticmethod def _assign_default_disabled_variant(evaluation_event: EvaluationEvent) -> None: @@ -289,10 +271,9 @@ def _check_feature_base(self, feature_flag_id: str) -> Tuple[EvaluationEvent, bo if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY): self._cache = {} self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY) - self._update_reversed_feature_flags() if not self._cache.get(feature_flag_id): - feature_flag = _get_feature_flag(self._configuration, feature_flag_id, self._reversed_feature_flags) + feature_flag = _get_feature_flag(self._configuration, feature_flag_id) self._cache[feature_flag_id] = feature_flag else: feature_flag = self._cache.get(feature_flag_id) From 1021dada48589ca827f73fde944a557e9055c7f4 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Thu, 19 Jun 2025 12:56:34 -0700 Subject: [PATCH 6/9] Update featuremanagement/_featuremanagerbase.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- featuremanagement/_featuremanagerbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index c669b4f..7494f4a 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -65,7 +65,7 @@ def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: if not feature_flags or not isinstance(feature_flags, list): return [] - flag_ids = [feature_flag.get("id") for feature_flag in feature_flags] + flag_ids = [feature_flag.get("id") for feature_flag in feature_flags if feature_flag.get("id")] return list(dict.fromkeys(flag_ids)) From ab22c6ece410b02003ef87f57ca5711e2a97e4fb Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 19 Jun 2025 12:57:09 -0700 Subject: [PATCH 7/9] Update _featuremanagerbase.py --- featuremanagement/_featuremanagerbase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 7494f4a..6c60bb0 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -40,9 +40,9 @@ def _get_feature_flag(configuration: Mapping[str, Any], feature_flag_name: str) feature_flags = feature_management.get(FEATURE_FLAG_KEY) if not feature_flags or not isinstance(feature_flags, list): return None - + index = len(feature_flags) - 1 - + while index >= 0: if feature_flags[index].get("id") == feature_flag_name: return FeatureFlag.convert_from_json(feature_flags[index]) From 61ff753a195b31db4d65a8132f66e571866eaf97 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 19 Jun 2025 13:07:20 -0700 Subject: [PATCH 8/9] fix is --- .github/workflows/validate.yml | 2 +- tests/test_feature_manager.py | 12 ++++++------ tests/test_feature_manager_async.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 1a3e75e..5286deb 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -32,7 +32,7 @@ jobs: run: | pip install -r tests/requirements.txt pytest tests --doctest-modules --cov-report=xml --cov-report=html - - name: Analysing the samples with pylint + - name: Analysing the samples/tests with pylint run: | pip install -r samples/requirements.txt pylint --disable=missing-function-docstring,missing-class-docstring samples tests \ No newline at end of file diff --git a/tests/test_feature_manager.py b/tests/test_feature_manager.py index e1bafc7..00a93cf 100644 --- a/tests/test_feature_manager.py +++ b/tests/test_feature_manager.py @@ -180,7 +180,7 @@ def test_duplicate_feature_flags_last_wins(self): feature_manager = FeatureManager(feature_flags) # The last flag should win (enabled: true) - assert feature_manager.is_enabled("DuplicateFlag") == True + assert feature_manager.is_enabled("DuplicateFlag") is True # Should only list unique names flag_names = feature_manager.list_feature_flag_names() @@ -218,7 +218,7 @@ def test_duplicate_feature_flags_last_wins_disabled(self): feature_manager = FeatureManager(feature_flags) # The last flag should win (enabled: false) - assert feature_manager.is_enabled("DuplicateFlag") == False + assert feature_manager.is_enabled("DuplicateFlag") is False def test_duplicate_feature_flags_mixed_with_unique(self): """Test behavior with a mix of duplicate and unique feature flags.""" @@ -261,12 +261,12 @@ def test_duplicate_feature_flags_mixed_with_unique(self): feature_manager = FeatureManager(feature_flags) # Test unique flags work as expected - assert feature_manager.is_enabled("UniqueFlag1") == True - assert feature_manager.is_enabled("UniqueFlag2") == False - assert feature_manager.is_enabled("UniqueFlag3") == True + assert feature_manager.is_enabled("UniqueFlag1") is True + assert feature_manager.is_enabled("UniqueFlag2") is False + assert feature_manager.is_enabled("UniqueFlag3") is True # Test duplicate flag - last should win (enabled: true) - assert feature_manager.is_enabled("DuplicateFlag") == True + assert feature_manager.is_enabled("DuplicateFlag") is True # Test list includes all unique names flag_names = feature_manager.list_feature_flag_names() diff --git a/tests/test_feature_manager_async.py b/tests/test_feature_manager_async.py index e14bae9..e0cd71e 100644 --- a/tests/test_feature_manager_async.py +++ b/tests/test_feature_manager_async.py @@ -210,7 +210,7 @@ async def test_duplicate_feature_flags_last_wins_async(self): feature_manager = FeatureManager(feature_flags) # The last flag should win (enabled: true) - assert await feature_manager.is_enabled("DuplicateFlag") == True + assert await feature_manager.is_enabled("DuplicateFlag") is True # Should only list unique names flag_names = feature_manager.list_feature_flag_names() @@ -249,7 +249,7 @@ async def test_duplicate_feature_flags_last_wins_disabled_async(self): feature_manager = FeatureManager(feature_flags) # The last flag should win (enabled: false) - assert await feature_manager.is_enabled("DuplicateFlag") == False + assert await feature_manager.is_enabled("DuplicateFlag") is False class AlwaysOn(FeatureFilter): From 4ec52ad6dd4599b922e88e9f000a2629e0a2979f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Jun 2025 05:15:47 +0000 Subject: [PATCH 9/9] Replace dict.fromkeys() with set for simpler unique flag name deduplication Co-authored-by: zhiyuanliang-ms <141655842+zhiyuanliang-ms@users.noreply.github.com> --- featuremanagement/_featuremanagerbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/featuremanagement/_featuremanagerbase.py b/featuremanagement/_featuremanagerbase.py index 6c60bb0..2f0a58a 100644 --- a/featuremanagement/_featuremanagerbase.py +++ b/featuremanagement/_featuremanagerbase.py @@ -66,7 +66,7 @@ def _list_feature_flag_names(configuration: Mapping[str, Any]) -> List[str]: return [] flag_ids = [feature_flag.get("id") for feature_flag in feature_flags if feature_flag.get("id")] - return list(dict.fromkeys(flag_ids)) + return list(set(flag_ids)) class FeatureManagerBase(ABC):