diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f6d0b0fb..ca1ca59a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -45,7 +45,7 @@ jobs:
- if: matrix.python-version == '3.11'
name: Upload coverage to Codecov
- uses: codecov/codecov-action@v4.3.0
+ uses: codecov/codecov-action@v4.5.0
with:
flags: unittests # optional
name: coverage # optional
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7669ac06..8047124a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,17 +16,16 @@ permissions: # added using https://github.com/step-security/secure-workflows
jobs:
release-please:
permissions:
- contents: write # for google-github-actions/release-please-action to create release commit
- pull-requests: write # for google-github-actions/release-please-action to create release PR
+ contents: write # for googleapis/release-please-action to create release commit
+ pull-requests: write # for googleapis/release-please-action to create release PR
runs-on: ubuntu-latest
steps:
- - uses: google-github-actions/release-please-action@v4
+ - uses: googleapis/release-please-action@v4
id: release
with:
- command: manifest
- token: ${{secrets.GITHUB_TOKEN}}
- default-branch: main
+ token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
+ target-branch: main
outputs:
release_created: ${{ steps.release.outputs.release_created }}
release_tag_name: ${{ steps.release.outputs.tag_name }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6cb055ad..86bea701 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.4.2
+ rev: v0.5.6
hooks:
- id: ruff
args: [--fix]
@@ -16,7 +16,7 @@ repos:
- id: check-merge-conflict
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.10.0
+ rev: v1.11.1
hooks:
- id: mypy
files: openfeature
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 0d62e08c..e961dcba 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1 +1 @@
-{".":"0.7.0"}
\ No newline at end of file
+{".":"0.7.1"}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 250e5a10..a8f09900 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,45 @@
# Changelog
+## [0.7.1](https://github.com/open-feature/python-sdk/compare/v0.7.0...v0.7.1) (2024-08-02)
+
+
+### ๐ Bug Fixes
+
+* event handler methods are not thread-safe ([#329](https://github.com/open-feature/python-sdk/issues/329)) ([3217575](https://github.com/open-feature/python-sdk/commit/3217575f4f87587751e47707384c344c185b684c)), closes [#326](https://github.com/open-feature/python-sdk/issues/326)
+* make global hooks thread safe ([#331](https://github.com/open-feature/python-sdk/issues/331)) ([5abcf3b](https://github.com/open-feature/python-sdk/commit/5abcf3b157f0f1ef6655a64abf1229ab84ad190e))
+* remove exception logging during evaluation ([#347](https://github.com/open-feature/python-sdk/issues/347)) ([0ed625f](https://github.com/open-feature/python-sdk/commit/0ed625f18617472ac0e60a88e727223381d8d735))
+
+
+### ๐งน Chore
+
+* **deps:** update codecov/codecov-action action to v4.3.1 ([#327](https://github.com/open-feature/python-sdk/issues/327)) ([f352045](https://github.com/open-feature/python-sdk/commit/f3520450557c71d8bfd7884c909114c27ba4e2e6))
+* **deps:** update codecov/codecov-action action to v4.4.0 ([#334](https://github.com/open-feature/python-sdk/issues/334)) ([6acbef9](https://github.com/open-feature/python-sdk/commit/6acbef94e67fa1c5da8f764a1d581870d92729aa))
+* **deps:** update codecov/codecov-action action to v4.4.1 ([#335](https://github.com/open-feature/python-sdk/issues/335)) ([fa67709](https://github.com/open-feature/python-sdk/commit/fa677092f894ed0ad00093391b799fb5a2adbab2))
+* **deps:** update codecov/codecov-action action to v4.5.0 ([#341](https://github.com/open-feature/python-sdk/issues/341)) ([e6a353e](https://github.com/open-feature/python-sdk/commit/e6a353e4754aa9443f3042b820bf167b6a66c944))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.10 ([#344](https://github.com/open-feature/python-sdk/issues/344)) ([2a45af8](https://github.com/open-feature/python-sdk/commit/2a45af895cc7dc7e15f94422a9de58d2b82db92b))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.3 ([#330](https://github.com/open-feature/python-sdk/issues/330)) ([f8544ff](https://github.com/open-feature/python-sdk/commit/f8544ffaf6abdee88d38e40c2dc493b36dad2c82))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.4 ([#333](https://github.com/open-feature/python-sdk/issues/333)) ([bd0bc1e](https://github.com/open-feature/python-sdk/commit/bd0bc1e2b7a28b1f1dcd50b63b61214131968925))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.5 ([#336](https://github.com/open-feature/python-sdk/issues/336)) ([2f93524](https://github.com/open-feature/python-sdk/commit/2f9352406301d0dfb804d04bf21039e87eeb01c5))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.6 ([#337](https://github.com/open-feature/python-sdk/issues/337)) ([cf61e5b](https://github.com/open-feature/python-sdk/commit/cf61e5b682481b4350d47af928f330bbbd93d7f1))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.7 ([#338](https://github.com/open-feature/python-sdk/issues/338)) ([1bf4682](https://github.com/open-feature/python-sdk/commit/1bf4682b466b998106fb94c7bbafdaa4a5e32289))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.8 ([#339](https://github.com/open-feature/python-sdk/issues/339)) ([44b0787](https://github.com/open-feature/python-sdk/commit/44b07879b08030c1356192ad4f69bc8b58c59914))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.4.9 ([#342](https://github.com/open-feature/python-sdk/issues/342)) ([f3982dc](https://github.com/open-feature/python-sdk/commit/f3982dc8c6faf5de6b86a406f8ecf2056d15026b))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.0 ([#346](https://github.com/open-feature/python-sdk/issues/346)) ([5c7bd14](https://github.com/open-feature/python-sdk/commit/5c7bd14b415336e990aced2bf2b12f6d2dd64b84))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.1 ([#348](https://github.com/open-feature/python-sdk/issues/348)) ([5dff1e8](https://github.com/open-feature/python-sdk/commit/5dff1e89b21542a16d602a541f76a52f8a0dbc4f))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.2 ([#349](https://github.com/open-feature/python-sdk/issues/349)) ([299a4f4](https://github.com/open-feature/python-sdk/commit/299a4f4630c18c8fc5a5bb1a55a1bcaa9a19fd8c))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.3 ([#350](https://github.com/open-feature/python-sdk/issues/350)) ([fe63b64](https://github.com/open-feature/python-sdk/commit/fe63b64d8fe90efc1433971aa7b1701ef8ae93c9))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.4 ([#352](https://github.com/open-feature/python-sdk/issues/352)) ([c294689](https://github.com/open-feature/python-sdk/commit/c29468941b946d6b8e355c3d60bc2e1f14faa959))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.5 ([#353](https://github.com/open-feature/python-sdk/issues/353)) ([6d46d95](https://github.com/open-feature/python-sdk/commit/6d46d957bdd8dd58bf11ab47689dbc8e19e80cf6))
+* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.5.6 ([#356](https://github.com/open-feature/python-sdk/issues/356)) ([261aa41](https://github.com/open-feature/python-sdk/commit/261aa4168ef7aab4d8613af4f45df1a495018f2e))
+* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.10.1 ([#345](https://github.com/open-feature/python-sdk/issues/345)) ([b553dfa](https://github.com/open-feature/python-sdk/commit/b553dfa607ce3c22d1369180c7b8a20291895ac0))
+* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.11.0 ([#351](https://github.com/open-feature/python-sdk/issues/351)) ([931e0cb](https://github.com/open-feature/python-sdk/commit/931e0cb3a8515dcb46c37c3eb9fa2bc08d88eed6))
+* **deps:** update pre-commit hook pre-commit/mirrors-mypy to v1.11.1 ([#355](https://github.com/open-feature/python-sdk/issues/355)) ([62c4b67](https://github.com/open-feature/python-sdk/commit/62c4b672f67f2d8c9e98c5fab902542e5d2092b2))
+
+
+### ๐ Refactoring
+
+* bind providers explicitly to a registry with attach/detach ([#324](https://github.com/open-feature/python-sdk/issues/324)) ([c3ad697](https://github.com/open-feature/python-sdk/commit/c3ad697a80ade72fb5cdee147ac5c11c38e6533f))
+
## [0.7.0](https://github.com/open-feature/python-sdk/compare/v0.6.1...v0.7.0) (2024-04-25)
diff --git a/README.md b/README.md
index ba27906e..414a3514 100644
--- a/README.md
+++ b/README.md
@@ -19,8 +19,8 @@
-
-
+
+
@@ -60,13 +60,13 @@
#### Pip install
```bash
-pip install openfeature-sdk==0.7.0
+pip install openfeature-sdk==0.7.1
```
#### requirements.txt
```bash
-openfeature-sdk==0.7.0
+openfeature-sdk==0.7.1
```
```python
diff --git a/openfeature/_event_support.py b/openfeature/_event_support.py
index 26753b05..42e6250b 100644
--- a/openfeature/_event_support.py
+++ b/openfeature/_event_support.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import threading
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List
@@ -15,7 +16,10 @@
from openfeature.client import OpenFeatureClient
+_global_lock = threading.RLock()
_global_handlers: Dict[ProviderEvent, List[EventHandler]] = defaultdict(list)
+
+_client_lock = threading.RLock()
_client_handlers: Dict[OpenFeatureClient, Dict[ProviderEvent, List[EventHandler]]] = (
defaultdict(lambda: defaultdict(list))
)
@@ -24,20 +28,23 @@
def run_client_handlers(
client: OpenFeatureClient, event: ProviderEvent, details: EventDetails
) -> None:
- for handler in _client_handlers[client][event]:
- handler(details)
+ with _client_lock:
+ for handler in _client_handlers[client][event]:
+ handler(details)
def run_global_handlers(event: ProviderEvent, details: EventDetails) -> None:
- for handler in _global_handlers[event]:
- handler(details)
+ with _global_lock:
+ for handler in _global_handlers[event]:
+ handler(details)
def add_client_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
- handlers = _client_handlers[client][event]
- handlers.append(handler)
+ with _client_lock:
+ handlers = _client_handlers[client][event]
+ handlers.append(handler)
_run_immediate_handler(client, event, handler)
@@ -45,12 +52,14 @@ def add_client_handler(
def remove_client_handler(
client: OpenFeatureClient, event: ProviderEvent, handler: EventHandler
) -> None:
- handlers = _client_handlers[client][event]
- handlers.remove(handler)
+ with _client_lock:
+ handlers = _client_handlers[client][event]
+ handlers.remove(handler)
def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
- _global_handlers[event].append(handler)
+ with _global_lock:
+ _global_handlers[event].append(handler)
from openfeature.api import get_client
@@ -58,7 +67,8 @@ def add_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
def remove_global_handler(event: ProviderEvent, handler: EventHandler) -> None:
- _global_handlers[event].remove(handler)
+ with _global_lock:
+ _global_handlers[event].remove(handler)
def run_handlers_for_provider(
@@ -72,9 +82,10 @@ def run_handlers_for_provider(
# run the global handlers
run_global_handlers(event, details)
# run the handlers for clients associated to this provider
- for client in _client_handlers:
- if client.provider == provider:
- run_client_handlers(client, event, details)
+ with _client_lock:
+ for client in _client_handlers:
+ if client.provider == provider:
+ run_client_handlers(client, event, details)
def _run_immediate_handler(
@@ -91,5 +102,7 @@ def _run_immediate_handler(
def clear() -> None:
- _global_handlers.clear()
- _client_handlers.clear()
+ with _global_lock:
+ _global_handlers.clear()
+ with _client_lock:
+ _client_handlers.clear()
diff --git a/openfeature/api.py b/openfeature/api.py
index c04d423e..c95d10ac 100644
--- a/openfeature/api.py
+++ b/openfeature/api.py
@@ -58,7 +58,6 @@ def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata:
def get_evaluation_context() -> EvaluationContext:
- global _evaluation_context
return _evaluation_context
@@ -80,7 +79,6 @@ def clear_hooks() -> None:
def get_hooks() -> typing.List[Hook]:
- global _hooks
return _hooks
diff --git a/openfeature/client.py b/openfeature/client.py
index 0429911a..9e4518ec 100644
--- a/openfeature/client.py
+++ b/openfeature/client.py
@@ -361,12 +361,6 @@ def evaluate_flag_details( # noqa: PLR0915
return flag_evaluation
except OpenFeatureError as err:
- logger.exception(
- "Error %s while evaluating flag with key: '%s'",
- err.error_code,
- flag_key,
- )
-
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
return FlagEvaluationDetails(
diff --git a/openfeature/hook/__init__.py b/openfeature/hook/__init__.py
index e1524ad1..e98301fa 100644
--- a/openfeature/hook/__init__.py
+++ b/openfeature/hook/__init__.py
@@ -23,7 +23,7 @@ class HookType(Enum):
class HookContext:
- def __init__( # noqa: PLR0913
+ def __init__(
self,
flag_key: str,
flag_type: FlagType,
diff --git a/openfeature/provider/__init__.py b/openfeature/provider/__init__.py
index 95fdac5d..8927551e 100644
--- a/openfeature/provider/__init__.py
+++ b/openfeature/provider/__init__.py
@@ -23,6 +23,15 @@ class ProviderStatus(Enum):
class FeatureProvider(typing.Protocol): # pragma: no cover
+ def attach(
+ self,
+ on_emit: typing.Callable[
+ [FeatureProvider, ProviderEvent, ProviderEventDetails], None
+ ],
+ ) -> None: ...
+
+ def detach(self) -> None: ...
+
def initialize(self, evaluation_context: EvaluationContext) -> None: ...
def shutdown(self) -> None: ...
@@ -68,6 +77,18 @@ def resolve_object_details(
class AbstractProvider(FeatureProvider):
+ def attach(
+ self,
+ on_emit: typing.Callable[
+ [FeatureProvider, ProviderEvent, ProviderEventDetails], None
+ ],
+ ) -> None:
+ self._on_emit = on_emit
+
+ def detach(self) -> None:
+ if hasattr(self, "_on_emit"):
+ del self._on_emit
+
def initialize(self, evaluation_context: EvaluationContext) -> None:
pass
@@ -141,6 +162,5 @@ def emit_provider_stale(self, details: ProviderEventDetails) -> None:
self.emit(ProviderEvent.PROVIDER_STALE, details)
def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None:
- from openfeature.provider._registry import provider_registry
-
- provider_registry.dispatch_event(self, event, details)
+ if hasattr(self, "_on_emit"):
+ self._on_emit(self, event, details)
diff --git a/openfeature/provider/_registry.py b/openfeature/provider/_registry.py
index 902a204e..e2ec2e53 100644
--- a/openfeature/provider/_registry.py
+++ b/openfeature/provider/_registry.py
@@ -71,6 +71,7 @@ def _get_evaluation_context(self) -> EvaluationContext:
return get_evaluation_context()
def _initialize_provider(self, provider: FeatureProvider) -> None:
+ provider.attach(self.dispatch_event)
try:
if hasattr(provider, "initialize"):
provider.initialize(self._get_evaluation_context())
@@ -106,6 +107,7 @@ def _shutdown_provider(self, provider: FeatureProvider) -> None:
error_code=ErrorCode.PROVIDER_FATAL,
),
)
+ provider.detach()
def get_provider_status(self, provider: FeatureProvider) -> ProviderStatus:
return self._provider_status.get(provider, ProviderStatus.NOT_READY)
diff --git a/openfeature/version.py b/openfeature/version.py
new file mode 100644
index 00000000..a5f830a2
--- /dev/null
+++ b/openfeature/version.py
@@ -0,0 +1 @@
+__version__ = "0.7.1"
diff --git a/pyproject.toml b/pyproject.toml
index fad55485..34b342a7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "openfeature_sdk"
-version = "0.7.0"
+version = "0.7.1"
description = "Standardizing Feature Flagging for Everyone"
readme = "README.md"
authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
diff --git a/release-please-config.json b/release-please-config.json
index 45dc90c5..2daf148a 100644
--- a/release-please-config.json
+++ b/release-please-config.json
@@ -1,5 +1,6 @@
{
"bootstrap-sha": "198336b098f167f858675235214cc907ede10182",
+ "signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>",
"packages": {
".": {
"release-type": "python",
diff --git a/tests/test_api.py b/tests/test_api.py
index aaea26b8..019037db 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -235,6 +235,8 @@ def test_clear_providers_shutdowns_every_provider_and_resets_default_provider():
def test_provider_events():
# Given
spy = MagicMock()
+ provider = NoOpProvider()
+ set_provider(provider)
add_handler(ProviderEvent.PROVIDER_READY, spy.provider_ready)
add_handler(
@@ -243,8 +245,6 @@ def test_provider_events():
add_handler(ProviderEvent.PROVIDER_ERROR, spy.provider_error)
add_handler(ProviderEvent.PROVIDER_STALE, spy.provider_stale)
- provider = NoOpProvider()
-
provider_details = ProviderEventDetails(message="message")
details = EventDetails.from_provider_event_details(
provider.get_metadata().name, provider_details
diff --git a/tests/test_client.py b/tests/test_client.py
index dc25abee..b51c460c 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,3 +1,6 @@
+import time
+import uuid
+from concurrent.futures import ThreadPoolExecutor
from unittest.mock import MagicMock
import pytest
@@ -356,3 +359,28 @@ def test_provider_event_late_binding():
# Then
spy.provider_configuration_changed.assert_called_once_with(details)
+
+
+def test_client_handlers_thread_safety():
+ provider = NoOpProvider()
+ set_provider(provider)
+
+ def add_handlers_task():
+ def handler(*args, **kwargs):
+ time.sleep(0.005)
+
+ for _ in range(10):
+ time.sleep(0.01)
+ client = get_client(str(uuid.uuid4()))
+ client.add_handler(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler)
+
+ def emit_events_task():
+ for _ in range(10):
+ time.sleep(0.01)
+ provider.emit_provider_configuration_changed(ProviderEventDetails())
+
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ f1 = executor.submit(add_handlers_task)
+ f2 = executor.submit(emit_events_task)
+ f1.result()
+ f2.result()