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 @@
-
-
+
+
@@ -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():