From ea7131fd01672543c25f216fef053c2b0ee66f25 Mon Sep 17 00:00:00 2001 From: Tim Zhu Date: Tue, 22 Sep 2026 18:21:19 -0400 Subject: [PATCH] Support Decider cohort beta on Python 3.9+ --- .github/workflows/python-package.yaml | 4 ++ setup.py | 2 +- tests/decider_tests.py | 84 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yaml b/.github/workflows/python-package.yaml index 23c9fde..08db664 100644 --- a/.github/workflows/python-package.yaml +++ b/.github/workflows/python-package.yaml @@ -20,10 +20,14 @@ jobs: reddit-decider-version: '1.15.0' - python-version: '3.9' reddit-decider-version: '1.18.1' + - python-version: '3.9' + reddit-decider-version: '1.19.0b1' - python-version: '3.10' reddit-decider-version: '1.15.0' - python-version: '3.10' reddit-decider-version: '1.18.1' + - python-version: '3.10' + reddit-decider-version: '1.19.0b1' steps: - uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index b441242..ad65871 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ "baseplate>=2.0.0a1,<3.0", "reddit-edgecontext>=1.0.0a3,<2.0", "reddit-decider>=1.15.0,<1.16.0; python_version < '3.9'", - "reddit-decider>=1.15.0,<1.19.0; python_version >= '3.9'", + "reddit-decider>=1.15.0,<1.20.0; python_version >= '3.9'", "typing_extensions>=3.10.0.0,<5.0", ], package_data={"reddit_experiments": ["py.typed"], "reddit_decider": ["py.typed"]}, diff --git a/tests/decider_tests.py b/tests/decider_tests.py index 3f8ada1..34cbf8e 100644 --- a/tests/decider_tests.py +++ b/tests/decider_tests.py @@ -4,6 +4,7 @@ import tempfile import unittest +from copy import deepcopy from importlib.metadata import version as package_version from unittest import mock @@ -12,6 +13,7 @@ from baseplate.lib.events import DebugLogger from packaging.version import Version from reddit_edgecontext import ValidatedAuthenticationToken +from rust_decider import init as init_manifest_validator from reddit_decider import Decider from reddit_decider import decider_client_from_config @@ -23,6 +25,24 @@ logger = logging.getLogger() DECIDER_SUPPORTS_PROPERTY_REGISTRY = Version(package_version("reddit-decider")) >= Version("1.18.1") +DECIDER_SUPPORTS_COHORT_FIELDS = Version(package_version("reddit-decider")) >= Version("1.19.0b1") + +COHORT_REGISTRY = { + "id": 1337, + "name": "$cohort_fields", + "version": "1", + "type": "dynamic_config", + "enabled": False, + "experiment": {"experiment_version": 1}, + "value_type": "Map", + "value": { + "$cohort_power_users": { + "object_type": "user", + "group_id": "cohort:power_users", + "value_field": "power_users", + } + }, +} USER_ID = "t2_1234" IS_LOGGED_IN = True @@ -564,6 +584,70 @@ def test_manifest_with_property_registry_preserves_experiments(self): self.assertEqual(variant, "variant_4") + @unittest.skipUnless( + DECIDER_SUPPORTS_COHORT_FIELDS, + "cohort registries require reddit-decider 1.19.0b1 or newer", + ) + def test_manifest_with_cohort_registry_preserves_experiments(self): + config = deepcopy(self.exp_base_config) + config["$cohort_fields"] = deepcopy(COHORT_REGISTRY) + + with create_temp_config_file(config) as f: + self.assertIsNone(init_manifest_validator("", f.name).err()) + decider = setup_decider(f, self.dc, self.mock_span, self.event_logger) + variant = decider.get_variant_without_expose(experiment_name="exp_1") + + self.assertEqual(variant, "variant_4") + + @unittest.skipUnless( + DECIDER_SUPPORTS_COHORT_FIELDS, + "cohort registries require reddit-decider 1.19.0b1 or newer", + ) + def test_invalid_cohort_definition_reports_manifest_error(self): + for key, value in [("group_id", ""), ("value_type", "unsupported")]: + with self.subTest(key=key): + config = deepcopy(self.exp_base_config) + config["$cohort_fields"] = deepcopy(COHORT_REGISTRY) + config["$cohort_fields"]["value"]["$cohort_power_users"][key] = value + + with create_temp_config_file(config) as f: + error = init_manifest_validator("", f.name).err() + self.assertIsNotNone(error) + self.assertIn("$cohort_fields", error) + with self.assertLogs("rust_decider", level="ERROR") as captured: + parsed = init_decider_parser(f) + self.assertIn("$cohort_fields", " ".join(captured.output)) + decider = Decider( + decider_context=self.dc, + internal=parsed, + server_span=self.mock_span, + context_name="test", + event_logger=self.event_logger, + ) + self.assertEqual(decider.get_variant_without_expose("exp_1"), "variant_4") + + @unittest.skipUnless( + DECIDER_SUPPORTS_COHORT_FIELDS, + "cohort registries require reddit-decider 1.19.0b1 or newer", + ) + def test_cohort_targeting_uses_typed_context_without_mutating_input(self): + config = deepcopy(self.exp_base_config) + config["$cohort_fields"] = deepcopy(COHORT_REGISTRY) + config["exp_1"]["experiment"]["targeting"] = { + "EQ": {"field": "$cohort_power_users", "value": True} + } + + with create_temp_config_file(config) as f: + for membership, expected in [(True, "variant_4"), (False, None), ("true", None)]: + with self.subTest(membership=membership): + fields = {"$cohort_power_users": membership} + ctx = DeciderContext(user_id=USER_ID, extracted_fields=fields) + decider = setup_decider(f, ctx, self.mock_span, self.event_logger) + + self.assertEqual(decider.get_variant_without_expose("exp_1"), expected) + self.assertEqual(fields, {"$cohort_power_users": membership}) + self.assertEqual(ctx.to_dict()["other_fields"], fields) + def test_none_returned_on_get_variant_call_with_bad_id(self): config = { "test": {