Skip to content

Commit fc81faa

Browse files
fix: use 'is True' for _is_internal_hook_provider check to prevent Mock false positives on Python <3.12
On Python <3.12, isinstance() against @runtime_checkable Protocols matches Mock objects (structural subtyping checks were tightened in 3.12). The existing getattr guard used a truthiness check, but Mock auto-creates attributes as truthy MagicMock objects, so the guard was ineffective. Switching to 'is True' identity check ensures only objects that explicitly set _is_internal_hook_provider = True (a literal bool) pass the guard. Also flattens nested conditional in ComparisonStrategy.determine_final_result. Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
1 parent 028df90 commit fc81faa

3 files changed

Lines changed: 20 additions & 20 deletions

File tree

openfeature/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def _as_internal_hook_provider(
475475
self, provider: FeatureProvider
476476
) -> InternalHookProvider | None:
477477
"""Return the provider as InternalHookProvider if it opts in, else None."""
478-
if getattr(provider, "_is_internal_hook_provider", False) and isinstance(
478+
if getattr(provider, "_is_internal_hook_provider", False) is True and isinstance(
479479
provider, InternalHookProvider
480480
):
481481
return provider

openfeature/provider/_registry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus:
127127
# We check _is_internal_hook_provider (a concrete class attribute) in
128128
# addition to isinstance, because runtime_checkable Protocols match any
129129
# object that has the right method names — including Mock objects.
130-
if getattr(provider, "_is_internal_hook_provider", False) and isinstance(
130+
if getattr(provider, "_is_internal_hook_provider", False) is True and isinstance(
131131
provider, InternalHookProvider
132132
):
133133
return provider.get_status()

openfeature/provider/multi_provider.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -305,26 +305,26 @@ def determine_final_result(
305305
# The first provider's result is the "final resolution" (used on agreement).
306306
# The fallback provider's result is used on mismatch (per JS SDK reference).
307307
final_evaluation = evaluations[0]
308-
fallback_evaluation = self._select_fallback_evaluation(evaluations)
309308
reference_value = final_evaluation.result.value
310-
has_mismatch = any(
309+
if not any(
311310
evaluation.result.value != reference_value for evaluation in evaluations
312-
)
313-
if has_mismatch:
314-
if self.on_mismatch is not None:
315-
mismatch_results = {
316-
evaluation.provider_name: evaluation.result
317-
for evaluation in evaluations
318-
}
319-
try:
320-
self.on_mismatch(flag_key, mismatch_results)
321-
except Exception:
322-
logger.exception(
323-
"Comparison strategy mismatch callback failed for flag '%s'",
324-
flag_key,
325-
)
326-
return fallback_evaluation.result
327-
return final_evaluation.result
311+
):
312+
return final_evaluation.result
313+
314+
fallback_evaluation = self._select_fallback_evaluation(evaluations)
315+
if self.on_mismatch is not None:
316+
mismatch_results = {
317+
evaluation.provider_name: evaluation.result
318+
for evaluation in evaluations
319+
}
320+
try:
321+
self.on_mismatch(flag_key, mismatch_results)
322+
except Exception:
323+
logger.exception(
324+
"Comparison strategy mismatch callback failed for flag '%s'",
325+
flag_key,
326+
)
327+
return fallback_evaluation.result
328328

329329
def _select_fallback_evaluation(
330330
self, evaluations: list[_ProviderEvaluation[FlagValueType]]

0 commit comments

Comments
 (0)