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