diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a61b870c..ea879d03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - if: matrix.python-version == '3.11' name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.0.7 + uses: codecov/codecov-action@v5.3.1 with: flags: unittests # optional name: coverage # optional diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ceec4eb..29045d87 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,8 +24,10 @@ jobs: - uses: googleapis/release-please-action@v4 id: release with: + command: manifest token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}} - target-branch: main + default-branch: main + signoff: "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>" outputs: release_created: ${{ steps.release.outputs.release_created }} release_tag_name: ${{ steps.release.outputs.tag_name }} @@ -37,12 +39,14 @@ jobs: # IMPORTANT: this permission is mandatory for trusted publishing to pypi id-token: write needs: release-please - if: ${{ needs.release-please.outputs.release_created }} - container: - image: "python:3.13" + if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 + with: + python-version: '3.13' - name: Upgrade pip run: pip install --upgrade pip @@ -54,5 +58,4 @@ jobs: run: hatch build - name: Publish a Python distribution to PyPI - # pinning till fixed https://github.com/pypa/gh-action-pypi-publish/issues/300 - uses: pypa/gh-action-pypi-publish@release/v1.11 + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitmodules b/.gitmodules index 61d2eb45..85115b56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "test-harness"] - path = test-harness - url = https://github.com/open-feature/test-harness.git +[submodule "spec"] + path = spec + url = https://github.com/open-feature/spec.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2b32957..c8748350 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 + rev: v0.9.4 hooks: - id: ruff args: [--fix] @@ -16,7 +16,7 @@ repos: - id: check-merge-conflict - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.13.0 + rev: v1.14.1 hooks: - id: mypy files: openfeature diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 967ffcc2..ca445a1c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"0.7.4"} \ No newline at end of file +{".":"0.7.5"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f734242..f0160f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ # Changelog +## [0.7.5](https://github.com/open-feature/python-sdk/compare/v0.7.4...v0.7.5) (2025-01-31) + + +### โš  BREAKING CHANGES + +The signature of the `finally_after` hook stage has been changed. The signature now includes the `evaluation details`, as per the [OpenFeature specification](https://openfeature.dev/specification/sections/hooks#requirement-438). To migrate, update any hook that implements the `finally_after` stage to accept `evaluation details` as the second argument. + +* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca)) + + +### ๐Ÿ› Bug Fixes + +* Finally hooks do not get called when the provider is not ready [#424](https://github.com/open-feature/python-sdk/issues/424) ([#425](https://github.com/open-feature/python-sdk/issues/425)) ([8f2caba](https://github.com/open-feature/python-sdk/commit/8f2cabaa32f595304ecd6964b6ae21909672ef4a)) + + +### โœจ New Features + +* Add evaluation details to finally hook stage [#403](https://github.com/open-feature/python-sdk/issues/403) ([#423](https://github.com/open-feature/python-sdk/issues/423)) ([9e9bb5c](https://github.com/open-feature/python-sdk/commit/9e9bb5c6269cfa5d9c9ffc7141c6dc63e399cdca)) +* Update test harness (add assertions) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#415](https://github.com/open-feature/python-sdk/issues/415)) ([f559d1b](https://github.com/open-feature/python-sdk/commit/f559d1b27a096c585bd81f32f6472039c9ce5e03)) +* Update test harness (copy test files) [#1467](https://github.com/open-feature/python-sdk/issues/1467) ([#416](https://github.com/open-feature/python-sdk/issues/416)) ([192f7c4](https://github.com/open-feature/python-sdk/commit/192f7c40bd07616030e86ff2aba7e993390d4af4)) + + +### ๐Ÿงน Chore + +* **config:** migrate config renovate.json ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e)) +* **config:** migrate renovate config ([#408](https://github.com/open-feature/python-sdk/issues/408)) ([26bc964](https://github.com/open-feature/python-sdk/commit/26bc9642270f9371170ce8ab2d9b938a0fca187e)) +* **deps:** update actions/setup-python digest to 4237552 ([#422](https://github.com/open-feature/python-sdk/issues/422)) ([9c2ed71](https://github.com/open-feature/python-sdk/commit/9c2ed71c6efdc2ece9ae89cf91f3b019c44b0033)) +* **deps:** update codecov/codecov-action action to v5.1.0 ([#401](https://github.com/open-feature/python-sdk/issues/401)) ([0459330](https://github.com/open-feature/python-sdk/commit/0459330cb91e9b28a15bdd380aec4c56c3b5d8df)) +* **deps:** update codecov/codecov-action action to v5.1.1 ([#402](https://github.com/open-feature/python-sdk/issues/402)) ([a6907d6](https://github.com/open-feature/python-sdk/commit/a6907d610e6dde1eecef56f25f3cc6a569b6eee4)) +* **deps:** update codecov/codecov-action action to v5.1.2 ([#405](https://github.com/open-feature/python-sdk/issues/405)) ([1c56480](https://github.com/open-feature/python-sdk/commit/1c564804afad474151489d695af7fa0409a768c6)) +* **deps:** update codecov/codecov-action action to v5.2.0 ([#418](https://github.com/open-feature/python-sdk/issues/418)) ([b69e81a](https://github.com/open-feature/python-sdk/commit/b69e81a63676240ef2abb98e96d2954a50f0c20a)) +* **deps:** update codecov/codecov-action action to v5.3.0 ([#420](https://github.com/open-feature/python-sdk/issues/420)) ([6af37b1](https://github.com/open-feature/python-sdk/commit/6af37b1c2bc2e10161673af5726932129ed02506)) +* **deps:** update codecov/codecov-action action to v5.3.1 ([#421](https://github.com/open-feature/python-sdk/issues/421)) ([e99e481](https://github.com/open-feature/python-sdk/commit/e99e481524ccfbea5bc7554d531f9c883dce6b5f)) +* **deps:** update googleapis/release-please-action action to v4 ([#428](https://github.com/open-feature/python-sdk/issues/428)) ([99905d5](https://github.com/open-feature/python-sdk/commit/99905d57f8b21a640f8f64f177679eee127ebba6)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.1 ([#398](https://github.com/open-feature/python-sdk/issues/398)) ([043385a](https://github.com/open-feature/python-sdk/commit/043385a8369e253a5e0ad1e184e980f8e8d7e5c7)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.2 ([#400](https://github.com/open-feature/python-sdk/issues/400)) ([2b6e210](https://github.com/open-feature/python-sdk/commit/2b6e210bc9dda72335e646fc60cde79b5bdd76c1)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.3 ([#404](https://github.com/open-feature/python-sdk/issues/404)) ([01ec388](https://github.com/open-feature/python-sdk/commit/01ec388d2d93fc87a4f2eca856cf507cbee35785)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.4 ([#406](https://github.com/open-feature/python-sdk/issues/406)) ([3296d3b](https://github.com/open-feature/python-sdk/commit/3296d3b229d41c7f879adfb7ab36e19de36617e4)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.8.6 ([#410](https://github.com/open-feature/python-sdk/issues/410)) ([7f9d422](https://github.com/open-feature/python-sdk/commit/7f9d422497a6d8392eee18ccfd12050eef7a2338)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.0 ([#411](https://github.com/open-feature/python-sdk/issues/411)) ([bc6e333](https://github.com/open-feature/python-sdk/commit/bc6e3332157788ec3f8037f68b9087e26a4634b5)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.1 ([#412](https://github.com/open-feature/python-sdk/issues/412)) ([cbace6a](https://github.com/open-feature/python-sdk/commit/cbace6a24c3fa091242aedd4f7c2e8de4332e463)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.2 ([#414](https://github.com/open-feature/python-sdk/issues/414)) ([9304292](https://github.com/open-feature/python-sdk/commit/9304292ea91580cf8cbfee7dbde922caa818189d)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.3 ([#419](https://github.com/open-feature/python-sdk/issues/419)) ([8f9cc7c](https://github.com/open-feature/python-sdk/commit/8f9cc7ca96a1210bab104e6342b4f7d1553bad3b)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.4 ([#426](https://github.com/open-feature/python-sdk/issues/426)) ([f726706](https://github.com/open-feature/python-sdk/commit/f72670689d25b82c8d54cacde3b1af179e0bc7e6)) +* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.0 ([#407](https://github.com/open-feature/python-sdk/issues/407)) ([89d6997](https://github.com/open-feature/python-sdk/commit/89d6997b1fe04df82e0873de7e7a1ea1aca2b071)) +* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.14.1 ([#409](https://github.com/open-feature/python-sdk/issues/409)) ([2c1840c](https://github.com/open-feature/python-sdk/commit/2c1840c87d00177d87b93135a38a7df98a9f6c0b)) + ## [0.7.4](https://github.com/open-feature/python-sdk/compare/v0.7.3...v0.7.4) (2024-11-25) diff --git a/README.md b/README.md index 8c078fab..5cc194d1 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ - - Latest version + + Latest version @@ -60,13 +60,13 @@ #### Pip install ```bash -pip install openfeature-sdk==0.7.4 +pip install openfeature-sdk==0.7.5 ``` #### requirements.txt ```bash -openfeature-sdk==0.7.4 +openfeature-sdk==0.7.5 ``` ```python diff --git a/openfeature/client.py b/openfeature/client.py index 1edfca63..cd82694b 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -295,37 +295,39 @@ def evaluate_flag_details( # noqa: PLR0915 reversed_merged_hooks = merged_hooks[:] reversed_merged_hooks.reverse() - status = self.get_provider_status() - if status == ProviderStatus.NOT_READY: - error_hooks( - flag_type, - hook_context, - ProviderNotReadyError(), - reversed_merged_hooks, - hook_hints, - ) - return FlagEvaluationDetails( - flag_key=flag_key, - value=default_value, - reason=Reason.ERROR, - error_code=ErrorCode.PROVIDER_NOT_READY, - ) - if status == ProviderStatus.FATAL: - error_hooks( - flag_type, - hook_context, - ProviderFatalError(), - reversed_merged_hooks, - hook_hints, - ) - return FlagEvaluationDetails( - flag_key=flag_key, - value=default_value, - reason=Reason.ERROR, - error_code=ErrorCode.PROVIDER_FATAL, - ) - try: + status = self.get_provider_status() + if status == ProviderStatus.NOT_READY: + error_hooks( + flag_type, + hook_context, + ProviderNotReadyError(), + reversed_merged_hooks, + hook_hints, + ) + flag_evaluation = FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=ErrorCode.PROVIDER_NOT_READY, + ) + return flag_evaluation + if status == ProviderStatus.FATAL: + error_hooks( + flag_type, + hook_context, + ProviderFatalError(), + reversed_merged_hooks, + hook_hints, + ) + flag_evaluation = FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=ErrorCode.PROVIDER_FATAL, + ) + return flag_evaluation + # https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md # Any resulting evaluation context from a before hook will overwrite # duplicate fields defined globally, on the client, or in the invocation. @@ -364,13 +366,14 @@ def evaluate_flag_details( # noqa: PLR0915 except OpenFeatureError as err: error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) - return FlagEvaluationDetails( + flag_evaluation = FlagEvaluationDetails( flag_key=flag_key, value=default_value, reason=Reason.ERROR, error_code=err.error_code, error_message=err.error_message, ) + return flag_evaluation # Catch any type of exception here since the user can provide any exception # in the error hooks except Exception as err: # pragma: no cover @@ -381,16 +384,23 @@ def evaluate_flag_details( # noqa: PLR0915 error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints) error_message = getattr(err, "error_message", str(err)) - return FlagEvaluationDetails( + flag_evaluation = FlagEvaluationDetails( flag_key=flag_key, value=default_value, reason=Reason.ERROR, error_code=ErrorCode.GENERAL, error_message=error_message, ) + return flag_evaluation finally: - after_all_hooks(flag_type, hook_context, reversed_merged_hooks, hook_hints) + after_all_hooks( + flag_type, + hook_context, + flag_evaluation, + reversed_merged_hooks, + hook_hints, + ) def _create_provider_evaluation( self, diff --git a/openfeature/hook/__init__.py b/openfeature/hook/__init__.py index 77190dc0..03d8c865 100644 --- a/openfeature/hook/__init__.py +++ b/openfeature/hook/__init__.py @@ -109,7 +109,12 @@ def error( """ pass - def finally_after(self, hook_context: HookContext, hints: HookHints) -> None: + def finally_after( + self, + hook_context: HookContext, + details: FlagEvaluationDetails[typing.Any], + hints: HookHints, + ) -> None: """ Run after flag evaluation, including any error processing. This will always run. Errors will be swallowed. diff --git a/openfeature/hook/_hook_support.py b/openfeature/hook/_hook_support.py index 349b25f3..2c151ae1 100644 --- a/openfeature/hook/_hook_support.py +++ b/openfeature/hook/_hook_support.py @@ -25,10 +25,11 @@ def error_hooks( def after_all_hooks( flag_type: FlagType, hook_context: HookContext, + details: FlagEvaluationDetails[typing.Any], hooks: typing.List[Hook], hints: typing.Optional[HookHints] = None, ) -> None: - kwargs = {"hook_context": hook_context, "hints": hints} + kwargs = {"hook_context": hook_context, "details": details, "hints": hints} _execute_hooks( flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs ) diff --git a/openfeature/version.py b/openfeature/version.py index ed9d4d87..ab55bb1a 100644 --- a/openfeature/version.py +++ b/openfeature/version.py @@ -1 +1 @@ -__version__ = "0.7.4" +__version__ = "0.7.5" diff --git a/pyproject.toml b/pyproject.toml index 394947a7..03180743 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "openfeature_sdk" -version = "0.7.4" +version = "0.7.5" description = "Standardizing Feature Flagging for Everyone" readme = "README.md" authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }] @@ -48,8 +48,8 @@ cov = [ "cov-report", ] e2e = [ - "git submodule update --init", - "cp test-harness/features/evaluation.feature tests/features/", + "git submodule add --force https://github.com/open-feature/spec.git spec", + "cp spec/specification/assets/gherkin/* tests/features/", "behave tests/features/", "rm tests/features/*.feature", ] diff --git a/release-please-config.json b/release-please-config.json index 2daf148a..45dc90c5 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,5 @@ { "bootstrap-sha": "198336b098f167f858675235214cc907ede10182", - "signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>", "packages": { ".": { "release-type": "python", diff --git a/renovate.json b/renovate.json index f29e38c2..6c501126 100644 --- a/renovate.json +++ b/renovate.json @@ -1,7 +1,7 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:base" + "config:recommended" ], "semanticCommits": "enabled", "pep621": { diff --git a/test-harness b/test-harness deleted file mode 160000 index bd13458f..00000000 --- a/test-harness +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bd13458f7e3587ab2ed98b8017bea3c2eb472cc9 diff --git a/tests/features/data.py b/tests/features/data.py index 0c84d14d..0ef36627 100644 --- a/tests/features/data.py +++ b/tests/features/data.py @@ -69,4 +69,16 @@ def context_func(flag: InMemoryFlag, evaluation_context: EvaluationContext): variants={"one": "uno", "two": "dos"}, default_variant="one", ), + "metadata-flag": InMemoryFlag( + state=InMemoryFlag.State.ENABLED, + default_variant="on", + variants={"on": True, "off": False}, + context_evaluator=None, + flag_metadata={ + "string": "1.0.2", + "integer": 2, + "float": 0.1, + "boolean": True, + }, + ), } diff --git a/tests/features/steps/flag_steps.py b/tests/features/steps/flag_steps.py new file mode 100644 index 00000000..574d6791 --- /dev/null +++ b/tests/features/steps/flag_steps.py @@ -0,0 +1,22 @@ +from behave import given, when + + +@given('a {flag_type}-flag with key "{flag_key}" and a default value "{default_value}"') +def step_impl_flag(context, flag_type: str, flag_key, default_value): + context.flag = (flag_type, flag_key, default_value) + + +@when("the flag was evaluated with details") +def step_impl_evaluation(context): + client = context.client + flag_type, key, default_value = context.flag + if flag_type.lower() == "string": + context.evaluation = client.get_string_details(key, default_value) + elif flag_type.lower() == "boolean": + context.evaluation = client.get_boolean_details(key, default_value) + elif flag_type.lower() == "object": + context.evaluation = client.get_object_details(key, default_value) + elif flag_type.lower() == "float": + context.evaluation = client.get_float_details(key, default_value) + elif flag_type.lower() == "integer": + context.evaluation = client.get_integer_details(key, default_value) diff --git a/tests/features/steps/hooks_steps.py b/tests/features/steps/hooks_steps.py new file mode 100644 index 00000000..bc7e156b --- /dev/null +++ b/tests/features/steps/hooks_steps.py @@ -0,0 +1,72 @@ +from unittest.mock import MagicMock + +from behave import then, when + +from openfeature.exception import ErrorCode +from openfeature.hook import Hook + + +@when("a hook is added to the client") +def step_impl_add_hook(context): + hook = MagicMock(spec=Hook) + hook.before = MagicMock() + hook.after = MagicMock() + hook.error = MagicMock() + hook.finally_after = MagicMock() + context.hook = hook + context.client.add_hooks([hook]) + + +@then("error hooks should be called") +def step_impl_call_error(context): + assert context.hook.before.called + assert context.hook.error.called + assert context.hook.finally_after.called + + +@then("non-error hooks should be called") +def step_impl_call_non_error(context): + assert context.hook.before.called + assert context.hook.after.called + assert context.hook.finally_after.called + + +def get_hook_from_name(context, hook_name): + if hook_name.lower() == "before": + return context.hook.before + elif hook_name.lower() == "after": + return context.hook.after + elif hook_name.lower() == "error": + return context.hook.error + elif hook_name.lower() == "finally": + return context.hook.finally_after + else: + raise ValueError(str(hook_name) + " is not a valid hook name") + + +def convert_value_from_flag_type(value, flag_type): + if value == "None": + return None + if flag_type.lower() == "boolean": + return bool(value) + elif flag_type.lower() == "integer": + return int(value) + elif flag_type.lower() == "float": + return float(value) + return value + + +@then('"{hook_names}" hooks should have evaluation details') +def step_impl_should_have_eval_details(context, hook_names): + for hook_name in hook_names.split(", "): + hook = get_hook_from_name(context, hook_name) + for row in context.table: + flag_type, key, value = row + + value = convert_value_from_flag_type(value, flag_type) + + actual = hook.call_args[1]["details"].__dict__[key] + if isinstance(actual, ErrorCode): + actual = str(actual) + + assert actual == value diff --git a/tests/features/steps/metadata_steps.py b/tests/features/steps/metadata_steps.py new file mode 100644 index 00000000..0154a9f0 --- /dev/null +++ b/tests/features/steps/metadata_steps.py @@ -0,0 +1,43 @@ +from behave import given, then + +from openfeature.api import get_client, set_provider +from openfeature.provider.in_memory_provider import InMemoryProvider +from tests.features.data import IN_MEMORY_FLAGS + + +@given("a stable provider") +def step_impl_stable_provider(context): + set_provider(InMemoryProvider(IN_MEMORY_FLAGS)) + context.client = get_client() + + +@then('the resolved metadata value "{key}" should be "{value}"') +def step_impl_check_metadata(context, key, value): + assert context.evaluation.flag_metadata[key] == value + + +@then("the resolved metadata is empty") +def step_impl_empty_metadata(context): + assert not context.evaluation.flag_metadata + + +@then("the resolved metadata should contain") +def step_impl_metadata_contains(context): + for row in context.table: + key, metadata_type, value = row + + assert context.evaluation.flag_metadata[ + key + ] == convert_value_from_metadata_type(value, metadata_type) + + +def convert_value_from_metadata_type(value, metadata_type): + if value == "None": + return None + if metadata_type.lower() == "boolean": + return bool(value) + elif metadata_type.lower() == "integer": + return int(value) + elif metadata_type.lower() == "float": + return float(value) + return value diff --git a/tests/features/steps/steps.py b/tests/features/steps/steps.py index ff517dfa..5d9d38fd 100644 --- a/tests/features/steps/steps.py +++ b/tests/features/steps/steps.py @@ -1,5 +1,7 @@ # flake8: noqa: F811 +from time import sleep + from behave import given, then, when from openfeature.api import get_client, set_provider @@ -17,7 +19,7 @@ 'the resolved {flag_type} details reason of flag with key "{key}" should be ' '"{reason}"' ) -def step_impl(context, flag_type, key, expected_reason): +def step_impl_resolved_should_be(context, flag_type, key, expected_reason): details: FlagEvaluationDetails = None if flag_type == "boolean": details = context.boolean_flag_details @@ -25,7 +27,13 @@ def step_impl(context, flag_type, key, expected_reason): @given("a provider is registered with cache disabled") -def step_impl(context): +def step_impl_provider_without_cache(context): + set_provider(InMemoryProvider(IN_MEMORY_FLAGS)) + context.client = get_client() + + +@given("a provider is registered") +def step_impl_provider(context): set_provider(InMemoryProvider(IN_MEMORY_FLAGS)) context.client = get_client() @@ -34,8 +42,9 @@ def step_impl(context): 'a {flag_type} flag with key "{key}" is evaluated with details and default value ' '"{default_value}"' ) -def step_impl(context, flag_type, key, default_value): - context.client = get_client() +def step_impl_evaluated_with_details(context, flag_type, key, default_value): + if context.client is None: + context.client = get_client() if flag_type == "boolean": context.boolean_flag_details = context.client.get_boolean_details( key, default_value @@ -50,7 +59,9 @@ def step_impl(context, flag_type, key, default_value): 'a boolean flag with key "{key}" is evaluated with {eval_details} and default ' 'value "{default_value}"' ) -def step_impl(context, key, eval_details, default_value): +def step_impl_bool_evaluated_with_details_and_default( + context, key, eval_details, default_value +): client: OpenFeatureClient = context.client context.boolean_flag_details = client.get_boolean_details(key, default_value) @@ -60,7 +71,7 @@ def step_impl(context, key, eval_details, default_value): 'a {flag_type} flag with key "{key}" is evaluated with default value ' '"{default_value}"' ) -def step_impl(context, flag_type, key, default_value): +def step_impl_evaluated_with_default(context, flag_type, key, default_value): client: OpenFeatureClient = context.client if flag_type == "boolean": @@ -70,12 +81,12 @@ def step_impl(context, flag_type, key, default_value): @then('the resolved string value should be "{expected_value}"') -def step_impl(context, expected_value): +def step_impl_resolved_string_should_be(context, expected_value): assert expected_value == context.string_flag_details.value @then('the resolved boolean value should be "{expected_value}"') -def step_impl(context, expected_value): +def step_impl_resolved_bool_should_be(context, expected_value): assert parse_boolean(expected_value) == context.boolean_flag_details.value @@ -83,7 +94,7 @@ def step_impl(context, expected_value): 'an integer flag with key "{key}" is evaluated with details and default value ' "{default_value:d}" ) -def step_impl(context, key, default_value): +def step_impl_int_evaluated_with_details_and_default(context, key, default_value): context.flag_key = key context.default_value = default_value context.integer_flag_details = context.client.get_integer_details( @@ -94,7 +105,7 @@ def step_impl(context, key, default_value): @when( 'an integer flag with key "{key}" is evaluated with default value {default_value:d}' ) -def step_impl(context, key, default_value): +def step_impl_int_evaluated_with_default(context, key, default_value): context.flag_key = key context.default_value = default_value context.integer_flag_details = context.client.get_integer_details( @@ -103,26 +114,26 @@ def step_impl(context, key, default_value): @when('a float flag with key "{key}" is evaluated with default value {default_value:f}') -def step_impl(context, key, default_value): +def step_impl_float_evaluated_with_default(context, key, default_value): context.flag_key = key context.default_value = default_value context.float_flag_details = context.client.get_float_details(key, default_value) @when('an object flag with key "{key}" is evaluated with a null default value') -def step_impl(context, key): +def step_impl_obj_evaluated_with_default(context, key): context.flag_key = key context.default_value = None context.object_flag_details = context.client.get_object_details(key, None) @then("the resolved integer value should be {expected_value:d}") -def step_impl(context, expected_value): +def step_impl_resolved_int_should_be(context, expected_value): assert expected_value == context.integer_flag_details.value @then("the resolved float value should be {expected_value:f}") -def step_impl(context, expected_value): +def step_impl_resolved_bool_should_be(context, expected_value): assert expected_value == context.float_flag_details.value @@ -131,7 +142,9 @@ def step_impl(context, expected_value): 'the resolved boolean details value should be "{expected_value}", the variant ' 'should be "{variant}", and the reason should be "{reason}"' ) -def step_impl(context, expected_value, variant, reason): +def step_impl_resolved_bool_should_be_with_reason( + context, expected_value, variant, reason +): assert parse_boolean(expected_value) == context.boolean_flag_details.value assert variant == context.boolean_flag_details.variant assert reason == context.boolean_flag_details.reason @@ -141,7 +154,9 @@ def step_impl(context, expected_value, variant, reason): 'the resolved string details value should be "{expected_value}", the variant ' 'should be "{variant}", and the reason should be "{reason}"' ) -def step_impl(context, expected_value, variant, reason): +def step_impl_resolved_string_should_be_with_reason( + context, expected_value, variant, reason +): assert expected_value == context.string_flag_details.value assert variant == context.string_flag_details.variant assert reason == context.string_flag_details.reason @@ -151,7 +166,9 @@ def step_impl(context, expected_value, variant, reason): 'the resolved object value should be contain fields "{field1}", "{field2}", and ' '"{field3}", with values "{val1}", "{val2}" and {val3}, respectively' ) -def step_impl(context, field1, field2, field3, val1, val2, val3): +def step_impl_resolved_obj_should_contain( + context, field1, field2, field3, val1, val2, val3 +): value = context.object_flag_details.value assert field1 in value assert field2 in value @@ -162,7 +179,7 @@ def step_impl(context, field1, field2, field3, val1, val2, val3): @then('the resolved flag value is "{flag_value}" when the context is empty') -def step_impl(context, flag_value): +def step_impl_resolved_is_with_empty_context(context, flag_value): context.string_flag_details = context.client.get_boolean_details( context.flag_key, context.default_value ) @@ -173,13 +190,13 @@ def step_impl(context, flag_value): "the reason should indicate an error and the error code should indicate a missing " 'flag with "{error_code}"' ) -def step_impl(context, error_code): +def step_impl_reason_should_indicate(context, error_code): assert context.string_flag_details.reason == Reason.ERROR assert context.string_flag_details.error_code == ErrorCode[error_code] @then("the default {flag_type} value should be returned") -def step_impl(context, flag_type): +def step_impl_return_default(context, flag_type): flag_details = getattr(context, f"{flag_type}_flag_details") assert context.default_value == flag_details.value @@ -188,7 +205,7 @@ def step_impl(context, flag_type): 'a float flag with key "{key}" is evaluated with details and default value ' "{default_value:f}" ) -def step_impl(context, key, default_value): +def step_impl_float_with_details(context, key, default_value): context.float_flag_details = context.client.get_float_details(key, default_value) @@ -196,7 +213,7 @@ def step_impl(context, key, default_value): "the resolved float details value should be {expected_value:f}, the variant should " 'be "{variant}", and the reason should be "{reason}"' ) -def step_impl(context, expected_value, variant, reason): +def step_impl_resolved_float_with_variant(context, expected_value, variant, reason): assert expected_value == context.float_flag_details.value assert variant == context.float_flag_details.variant assert reason == context.float_flag_details.reason @@ -205,7 +222,7 @@ def step_impl(context, expected_value, variant, reason): @when( 'an object flag with key "{key}" is evaluated with details and a null default value' ) -def step_impl(context, key): +def step_impl_eval_obj(context, key): context.object_flag_details = context.client.get_object_details(key, None) @@ -213,7 +230,7 @@ def step_impl(context, key): 'the resolved object details value should be contain fields "{field1}", "{field2}",' ' and "{field3}", with values "{val1}", "{val2}" and {val3}, respectively' ) -def step_impl(context, field1, field2, field3, val1, val2, val3): +def step_impl_eval_obj_with_fields(context, field1, field2, field3, val1, val2, val3): value = context.object_flag_details.value assert field1 in value assert field2 in value @@ -224,7 +241,7 @@ def step_impl(context, field1, field2, field3, val1, val2, val3): @then('the variant should be "{variant}", and the reason should be "{reason}"') -def step_impl(context, variant, reason): +def step_impl_variant(context, variant, reason): assert variant == context.object_flag_details.variant assert reason == context.object_flag_details.reason @@ -233,7 +250,7 @@ def step_impl(context, variant, reason): 'context contains keys "{key1}", "{key2}", "{key3}", "{key4}" with values "{val1}",' ' "{val2}", {val3:d}, "{val4}"' ) -def step_impl(context, key1, key2, key3, key4, val1, val2, val3, val4): +def step_impl_context(context, key1, key2, key3, key4, val1, val2, val3, val4): context.evaluation_context = EvaluationContext( None, { @@ -246,7 +263,7 @@ def step_impl(context, key1, key2, key3, key4, val1, val2, val3, val4): @when('a flag with key "{key}" is evaluated with default value "{default_value}"') -def step_impl(context, key, default_value): +def step_impl_flag_with_key_and_default(context, key, default_value): context.flag_key = key context.default_value = default_value context.string_flag_details = context.client.get_string_details( @@ -255,7 +272,7 @@ def step_impl(context, key, default_value): @then('the resolved string response should be "{expected_value}"') -def step_impl(context, expected_value): +def step_impl_reason(context, expected_value): assert expected_value == context.string_flag_details.value @@ -263,7 +280,7 @@ def step_impl(context, expected_value): 'a non-existent string flag with key "{flag_key}" is evaluated with details and a ' 'default value "{default_value}"' ) -def step_impl(context, flag_key, default_value): +def step_impl_non_existing(context, flag_key, default_value): context.flag_key = flag_key context.default_value = default_value context.string_flag_details = context.client.get_string_details( @@ -275,7 +292,7 @@ def step_impl(context, flag_key, default_value): 'a string flag with key "{flag_key}" is evaluated as an integer, with details and a' " default value {default_value:d}" ) -def step_impl(context, flag_key, default_value): +def step_impl_string_with_details(context, flag_key, default_value): context.flag_key = flag_key context.default_value = default_value context.integer_flag_details = context.client.get_integer_details( @@ -287,7 +304,7 @@ def step_impl(context, flag_key, default_value): "the reason should indicate an error and the error code should indicate a type " 'mismatch with "{error_code}"' ) -def step_impl(context, error_code): +def step_impl_type_mismatch(context, error_code): assert context.integer_flag_details.reason == Reason.ERROR assert context.integer_flag_details.error_code == ErrorCode[error_code] @@ -299,17 +316,17 @@ def step_impl(context, error_code): 'the flag\'s configuration with key "{key}" is updated to defaultVariant ' '"{variant}"' ) -def step_impl(context, key, variant): +def step_impl_config_update(context, key, variant): raise NotImplementedError("Step definition not implemented yet") @given("sleep for {duration} milliseconds") -def step_impl(context, duration): - raise NotImplementedError("Step definition not implemented yet") +def step_impl_sleep(context, duration): + sleep(float(duration) * 0.001) @then('the resolved string details reason should be "{reason}"') -def step_impl(context, reason): +def step_impl_reason_should_be(context, reason): raise NotImplementedError("Step definition not implemented yet") diff --git a/tests/hook/test_hook_support.py b/tests/hook/test_hook_support.py index 64bb8f6f..19a86ec0 100644 --- a/tests/hook/test_hook_support.py +++ b/tests/hook/test_hook_support.py @@ -137,12 +137,17 @@ def test_after_hooks_run_after_method(mock_hook): def test_finally_after_hooks_run_finally_after_method(mock_hook): # Given hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "") + flag_evaluation_details = FlagEvaluationDetails( + hook_context.flag_key, "val", "unknown" + ) hook_hints = MappingProxyType({}) # When - after_all_hooks(FlagType.BOOLEAN, hook_context, [mock_hook], hook_hints) + after_all_hooks( + FlagType.BOOLEAN, hook_context, flag_evaluation_details, [mock_hook], hook_hints + ) # Then mock_hook.supports_flag_value_type.assert_called_once() mock_hook.finally_after.assert_called_once() mock_hook.finally_after.assert_called_with( - hook_context=hook_context, hints=hook_hints + hook_context=hook_context, details=flag_evaluation_details, hints=hook_hints ) diff --git a/tests/test_client.py b/tests/test_client.py index 7f0ca461..f6002c18 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -221,6 +221,7 @@ def test_should_shortcircuit_if_provider_is_not_ready( assert flag_details.reason == Reason.ERROR assert flag_details.error_code == ErrorCode.PROVIDER_NOT_READY spy_hook.error.assert_called_once() + spy_hook.finally_after.assert_called_once() # Requirement 1.7.7 @@ -243,6 +244,7 @@ def test_should_shortcircuit_if_provider_is_in_irrecoverable_error_state( assert flag_details.reason == Reason.ERROR assert flag_details.error_code == ErrorCode.PROVIDER_FATAL spy_hook.error.assert_called_once() + spy_hook.finally_after.assert_called_once() def test_should_run_error_hooks_if_provider_returns_resolution_with_error_code():