From a03b9e7a09fa4d0285161dfc7855c7f4f238ac27 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Fri, 18 Sep 2026 12:53:04 +0200 Subject: [PATCH 1/2] chore(deps): move the workspace onto openfeature-sdk 0.10.0 The lock has resolved 0.8.4 since 2025-12-09 while 0.9.0 and 0.10.0 have shipped, so nothing in this repository has ever been tested against either. Nothing pinned it there: every floor is a `>=`, every member is `requires-python >=3.10`, and the lock carries no constraints -- renovate's lock-file maintenance simply never picked the release up. `uv lock --upgrade-package openfeature-sdk` moves 0.8.4 -> 0.10.0 and touches nothing else. Two behaviour changes reach the tests. Neither touches the flagd source, which needs no change and stays clean under `poe mypy` on 26 files. First, 0.10.0 made `set_provider` non-blocking, so a test that sets a provider and evaluates on the next line now races initialisation and reads PROVIDER_NOT_READY -- 20 failures across test_errors.py and test_metadata.py. `set_provider_and_wait` restores the old blocking behaviour with one difference that matters here: it re-raises whatever `initialize` raised, where `set_provider` only dispatched PROVIDER_ERROR. Several of these scenarios feed the provider a deliberately broken flag file, so that raise is the scenario rather than a failure, hence the `contextlib.suppress`; it is not papering over an unexpected error. Second, 0.10.0 isolated event-handler dispatch onto a ThreadPoolExecutor where 0.8.4 called handlers inline -- open-feature/python-sdk#599. test_grpc_sync_fail_deadline read a flag set by a PROVIDER_ERROR handler on the line after the call returned, which is now a race: `_run_initialize` submits the event and then re-raises, and submit is not run. Rather than wait on the handler, the assertion goes: it existed because the old `set_provider` blocked and swallowed the error, leaving the event as the only way to observe a failed init, and `pytest.raises(ProviderNotReadyError)` now carries that claim directly. What the handler would assert today is that the SDK's registry dispatches PROVIDER_ERROR when `initialize` raises -- SDK plumbing, uniform across providers, and not this suite's to cover. The deadline measurement it shared the test with is untouched, though it now has to wait rather than time a call that would return immediately. The other two handler-driven tests here -- test_invalid_flag_set_metadata and the e2e event steps -- already poll with a timeout and need nothing. The tests are the only thing that needs 0.10.0, so the floor goes in flagd's dev group and the runtime floor stays at 0.8.2. Measured, not assumed: flagd is 208 passed at 0.8.4 and 208 passed at 0.10.0 after this change, having been 188 passed / 20 failed in between. CI covers tests/e2e, which needs docker, and reported 783 passed on this branch with the handler race as its only failure. The other nine packages pass tests and mypy unchanged, except unleash, which the next commit fixes. Signed-off-by: Simon Schrottner --- .../openfeature-provider-flagd/pyproject.toml | 3 ++ .../tests/test_errors.py | 35 +++++++++---------- .../tests/test_metadata.py | 8 +++-- uv.lock | 10 +++--- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/providers/openfeature-provider-flagd/pyproject.toml b/providers/openfeature-provider-flagd/pyproject.toml index 2c1c1b3a..bb5d4d83 100644 --- a/providers/openfeature-provider-flagd/pyproject.toml +++ b/providers/openfeature-provider-flagd/pyproject.toml @@ -35,6 +35,9 @@ dev = [ "coverage[toml]>=7.10.0,<8.0.0", "grpcio-health-checking>=1.82.1,<2.0.0", "mypy>=1.18.0,<2.0.0", + # The tests call `api.set_provider_and_wait`, which arrived in 0.10.0. The + # runtime floor stays at 0.8.2 because the provider itself needs nothing newer. + "openfeature-sdk>=0.10.0", "poethepoet>=0.37.0", "pytest>=9.0.0,<10.0.0", "pytest-bdd>=8.1.0,<9.0.0", diff --git a/providers/openfeature-provider-flagd/tests/test_errors.py b/providers/openfeature-provider-flagd/tests/test_errors.py index e335fc1c..c35e228e 100644 --- a/providers/openfeature-provider-flagd/tests/test_errors.py +++ b/providers/openfeature-provider-flagd/tests/test_errors.py @@ -1,3 +1,4 @@ +import contextlib import os import time @@ -7,13 +8,19 @@ from openfeature.contrib.provider.flagd import FlagdProvider from openfeature.contrib.provider.flagd.config import ResolverType from openfeature.evaluation_context import EvaluationContext -from openfeature.event import ProviderEvent -from openfeature.exception import ErrorCode +from openfeature.exception import ( + ErrorCode, + OpenFeatureError, + ProviderNotReadyError, +) from openfeature.flag_evaluation import Reason def create_client(provider: FlagdProvider): - api.set_provider(provider) + # These flag files are deliberately broken, so init may fail; what the + # evaluation returns afterwards is the assertion. + with contextlib.suppress(OpenFeatureError): + api.set_provider_and_wait(provider) return api.get_client() @@ -119,23 +126,15 @@ def test_flag_disabled(): @pytest.mark.parametrize("wait", (500, 250)) def test_grpc_sync_fail_deadline(wait: int): - init_failed = False - - def fail(*args, **kwargs): - nonlocal init_failed - init_failed = True - - api.get_client().add_handler(ProviderEvent.PROVIDER_ERROR, fail) - t = time.time() - api.set_provider( - FlagdProvider( - resolver_type=ResolverType.IN_PROCESS, - port=99999, # dead port to test failure - deadline_ms=wait, + with pytest.raises(ProviderNotReadyError): + api.set_provider_and_wait( + FlagdProvider( + resolver_type=ResolverType.IN_PROCESS, + port=99999, # dead port to test failure + deadline_ms=wait, + ) ) - ) elapsed = time.time() - t assert abs(elapsed - wait * 0.001) < 0.17 - assert init_failed diff --git a/providers/openfeature-provider-flagd/tests/test_metadata.py b/providers/openfeature-provider-flagd/tests/test_metadata.py index 939af96f..ef8331c5 100644 --- a/providers/openfeature-provider-flagd/tests/test_metadata.py +++ b/providers/openfeature-provider-flagd/tests/test_metadata.py @@ -1,3 +1,4 @@ +import contextlib import os import time from time import sleep @@ -11,7 +12,7 @@ _validate_metadata, ) from openfeature.event import EventDetails, ProviderEvent -from openfeature.exception import ErrorCode, ParseError +from openfeature.exception import ErrorCode, OpenFeatureError, ParseError def create_client(file_name): @@ -21,7 +22,10 @@ def create_client(file_name): offline_flag_source_path=f"{path}/{file_name}", ) - api.set_provider(provider) + # Some of these flag files are deliberately broken, so init may fail; what + # the evaluation returns afterwards is the assertion. + with contextlib.suppress(OpenFeatureError): + api.set_provider_and_wait(provider) return api.get_client() diff --git a/uv.lock b/uv.lock index 3168e251..9cfe32d6 100644 --- a/uv.lock +++ b/uv.lock @@ -1868,7 +1868,7 @@ dev = [ [[package]] name = "openfeature-provider-flagd" -version = "0.5.1" +version = "0.5.2" source = { editable = "providers/openfeature-provider-flagd" } dependencies = [ { name = "cachebox" }, @@ -1885,6 +1885,7 @@ dev = [ { name = "coverage", extra = ["toml"] }, { name = "grpcio-health-checking" }, { name = "mypy" }, + { name = "openfeature-sdk" }, { name = "poethepoet" }, { name = "pytest" }, { name = "pytest-bdd" }, @@ -1910,6 +1911,7 @@ dev = [ { name = "coverage", extras = ["toml"], specifier = ">=7.10.0,<8.0.0" }, { name = "grpcio-health-checking", specifier = ">=1.82.1,<2.0.0" }, { name = "mypy", specifier = ">=1.18.0,<2.0.0" }, + { name = "openfeature-sdk", specifier = ">=0.10.0" }, { name = "poethepoet", specifier = ">=0.37.0" }, { name = "pytest", specifier = ">=9.0.0,<10.0.0" }, { name = "pytest-bdd", specifier = ">=8.1.0,<9.0.0" }, @@ -2075,11 +2077,11 @@ dev = [ [[package]] name = "openfeature-sdk" -version = "0.8.4" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/08/f6698d0614b8703170117b786bd77b7b0a04f3ee00f19fbe9b360d2dee69/openfeature_sdk-0.8.4.tar.gz", hash = "sha256:66abf71f928ec8c0db1111072bb0ef2635dfbd09510f77f4b548e5d0ea0e6c1a", size = 29676, upload-time = "2025-12-09T07:31:13.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/cfc684b7d8314398d476ae8ed515c10db99c4d7f950989db464b4ded12ce/openfeature_sdk-0.10.0.tar.gz", hash = "sha256:938c2540bdea4da3b01ef507517ee636f223a35abaaca845c5587e594151b052", size = 33516, upload-time = "2026-06-01T19:45:35.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/80/f6532778188c573cc83790b11abccde717d4c1442514e722d6bb6140e55c/openfeature_sdk-0.8.4-py3-none-any.whl", hash = "sha256:805ba090669798fc343ca9fdcbc56ff0f4b57bf6757533f0854d2021192e620a", size = 35986, upload-time = "2025-12-09T07:31:12.092Z" }, + { url = "https://files.pythonhosted.org/packages/da/44/8a4f5225e930ff0d999fd43f5d743a4babeb6c7e76dddc00f0e118878ef3/openfeature_sdk-0.10.0-py3-none-any.whl", hash = "sha256:75497ea75d73f684eef509a25f79ad6386368862e050af80ab70a44ae49b33e4", size = 38941, upload-time = "2026-06-01T19:45:33.011Z" }, ] [[package]] From 40af2728969c6bedbabfe50fd0de0179e7f81db8 Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Fri, 18 Sep 2026 12:53:17 +0200 Subject: [PATCH 2/2] fix(unleash): drop the track override so client.track no-ops openfeature-sdk 0.9.0 added tracking, and `OpenFeatureClient.track` calls `provider.track(tracking_event_name, merged_eval_context, tracking_event_details)` -- three positional arguments. The provider's own no-op declared `track(event_name, event_details=None)`, which takes two, so the call raises TypeError: UnleashProvider.track() takes from 2 to 3 positional arguments but 4 were given instead of doing nothing. Requirement 6.1.1 says the client's track MUST be a no-op when the provider does not implement tracking, so the method that exists to satisfy it was the thing breaking it. This is reachable from a released install, not just from this branch: the package declares `openfeature-sdk>=0.8.2`, so a fresh resolve takes 0.10.0 and any `client.track(...)` call raises. It went unnoticed because the workspace lock held 0.8.4, where the SDK has no tracking API at all and therefore never calls this method. `AbstractProvider.track` is already a no-op with the signature the client calls, so deleting the override is the whole fix. Widening the signature instead would mean importing `TrackingEventDetails` for the annotation, and `openfeature.track` does not exist before 0.9.0 -- with the floor at 0.8.2 that turns a broken `client.track` into a ModuleNotFoundError on import, which is worse. Raising the floor to 0.9.0 would also work, but the provider needs nothing from that release beyond a type it only uses to say it does nothing. Verified against both ends of the declared range: on 0.10.0, registering the provider and calling `client.track("my-event")` raised TypeError before and returns None after; on 0.8.4, importing the provider raises ModuleNotFoundError with the annotation and imports cleanly without it. mypy caught the narrow signature as two `[override]` errors against both `AbstractProvider` and `FeatureProvider`; it was the only type error the 0.10.0 move produced anywhere in the workspace. The existing `hasattr(provider, "track")` assertion could not catch it -- it holds either way -- so the test now calls track the way the SDK does, which is also what guards against the override coming back. Fixes #433 Signed-off-by: Simon Schrottner --- .../openfeature/contrib/provider/unleash/__init__.py | 12 ------------ .../tests/test_provider.py | 5 +++++ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/providers/openfeature-provider-unleash/src/openfeature/contrib/provider/unleash/__init__.py b/providers/openfeature-provider-unleash/src/openfeature/contrib/provider/unleash/__init__.py index 8c0d6fef..bc5985d0 100644 --- a/providers/openfeature-provider-unleash/src/openfeature/contrib/provider/unleash/__init__.py +++ b/providers/openfeature-provider-unleash/src/openfeature/contrib/provider/unleash/__init__.py @@ -136,18 +136,6 @@ def _unleash_event_callback(self, event: BaseEvent) -> None: self._status = ProviderStatus.READY self._event_manager.handle_unleash_event(event) - def track( - self, - event_name: str, - event_details: dict | None = None, - ) -> None: - """No-op tracking method. - - Tracking is not implemented for this provider. Per the OpenFeature spec, - when the provider doesn't support tracking, client.track calls should no-op. - """ - return None - def _build_unleash_context( self, evaluation_context: EvaluationContext | None = None ) -> dict[str, Any] | None: diff --git a/providers/openfeature-provider-unleash/tests/test_provider.py b/providers/openfeature-provider-unleash/tests/test_provider.py index c47eb21d..ea9902c8 100644 --- a/providers/openfeature-provider-unleash/tests/test_provider.py +++ b/providers/openfeature-provider-unleash/tests/test_provider.py @@ -119,6 +119,11 @@ def test_unleash_provider_all_methods_implemented(): assert hasattr(provider, "add_handler") assert hasattr(provider, "remove_handler") assert hasattr(provider, "track") + # Tracking is not implemented, so track is inherited from + # AbstractProvider and no-ops. Called the way OpenFeatureClient.track + # calls it -- positionally -- since a narrower override raises + # TypeError there. + assert provider.track("some-event", EvaluationContext(), None) is None provider.shutdown()