From ca0b6f48a8f3399fdceaf1c8d2aa90be654b4384 Mon Sep 17 00:00:00 2001 From: Tom Carrio Date: Sat, 29 Oct 2022 01:44:09 -0400 Subject: [PATCH] feat: process flag evaluation options in client - performs validation of options as dictionary before pulling hooks and hints - introduces immutable dictionary extension of dict following naming from 3.12 release - documented "upgrade" process for MappingProxyType Signed-off-by: Tom Carrio --- open_feature/immutable_dict/__init__.py | 0 .../immutable_dict/mapping_proxy_type.py | 28 +++++++++++++++++++ open_feature/open_feature_client.py | 26 +++++++++++++---- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 open_feature/immutable_dict/__init__.py create mode 100644 open_feature/immutable_dict/mapping_proxy_type.py diff --git a/open_feature/immutable_dict/__init__.py b/open_feature/immutable_dict/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/open_feature/immutable_dict/mapping_proxy_type.py b/open_feature/immutable_dict/mapping_proxy_type.py new file mode 100644 index 0000000..389b468 --- /dev/null +++ b/open_feature/immutable_dict/mapping_proxy_type.py @@ -0,0 +1,28 @@ +class MappingProxyType(dict): + """ + MappingProxyType is an immutable dictionary type, written to + support Python 3.8 with easy transition to 3.12 upon removal + of older versions. + + See: https://stackoverflow.com/a/72474524 + + When upgrading to Python 3.12, you can update all references + from: + `from open_feature.immutable_dict.mapping_proxy_type import MappingProxyType` + + to: + `from types import MappingProxyType` + """ + def __hash__(self): + return id(self) + + def _immutable(self, *args, **kws): + raise TypeError('immutable instance of dictionary') + + __setitem__ = _immutable + __delitem__ = _immutable + clear = _immutable + update = _immutable + setdefault = _immutable + pop = _immutable + popitem = _immutable diff --git a/open_feature/open_feature_client.py b/open_feature/open_feature_client.py index 8be455e..15ca661 100644 --- a/open_feature/open_feature_client.py +++ b/open_feature/open_feature_client.py @@ -16,6 +16,7 @@ from open_feature.hooks.hook_support import ( before_hooks, error_hooks, ) +from open_feature.immutable_dict.mapping_proxy_type import MappingProxyType from open_feature.open_feature_evaluation_context import api_evaluation_context from open_feature.provider.no_op_provider import NoOpProvider from open_feature.provider.provider import AbstractProvider @@ -182,6 +183,8 @@ class OpenFeatureClient: if evaluation_context is None: evaluation_context = EvaluationContext() + evaluation_hooks, hook_hints = self.__extract_evaluation_options(flag_evaluation_options) + hook_context = HookContext( flag_key=flag_key, flag_type=flag_type, @@ -197,7 +200,7 @@ class OpenFeatureClient: # Any resulting evaluation context from a before hook will overwrite # duplicate fields defined globally, on the client, or in the invocation. invocation_context = before_hooks( - flag_type, hook_context, merged_hooks, None + flag_type, hook_context, merged_hooks, hook_hints ) invocation_context.merge(ctx2=evaluation_context) @@ -213,12 +216,12 @@ class OpenFeatureClient: merged_context, ) - after_hooks(type, hook_context, flag_evaluation, merged_hooks, None) + after_hooks(type, hook_context, flag_evaluation, merged_hooks, hook_hints) return flag_evaluation except OpenFeatureError as e: - error_hooks(flag_type, hook_context, e, merged_hooks, None) + error_hooks(flag_type, hook_context, e, merged_hooks, hook_hints) return FlagEvaluationDetails( flag_key=flag_key, value=default_value, @@ -229,7 +232,7 @@ class OpenFeatureClient: # Catch any type of exception here since the user can provide any exception # in the error hooks except Exception as e: # noqa - error_hooks(flag_type, hook_context, e, merged_hooks, None) + error_hooks(flag_type, hook_context, e, merged_hooks, hook_hints) error_message = getattr(e, "error_message", str(e)) return FlagEvaluationDetails( flag_key=flag_key, @@ -240,7 +243,7 @@ class OpenFeatureClient: ) finally: - after_all_hooks(flag_type, hook_context, merged_hooks, None) + after_all_hooks(flag_type, hook_context, merged_hooks, hook_hints) def _create_provider_evaluation( self, @@ -280,3 +283,16 @@ class OpenFeatureClient: raise GeneralError(error_message="Unknown flag type") return get_details_callable(*args) + + def __extract_evaluation_options(self, flag_evaluation_options: typing.Any) -> typing.Tuple(typing.List[Hook], MappingProxyType): + evaluation_hooks: typing.List[Hook] = [] + hook_hints: dict = {} + + if flag_evaluation_options is dict: + if 'hook_hints' in flag_evaluation_options and flag_evaluation_options['hook_hints'] is dict: + hook_hints = dict(flag_evaluation_options['hook_hints']) + + if 'hooks' in flag_evaluation_options and flag_evaluation_options['hooks'] is list: + evaluation_hooks = flag_evaluation_options['hooks'] + + return (evaluation_hooks, MappingProxyType(hook_hints)) \ No newline at end of file