From a4a20ca91f4b6851d29bd57e488b54d57fa9496d Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Thu, 20 Jul 2023 09:02:48 +0100 Subject: [PATCH] Add FeatureFlag class --- bugsnag/feature_flags.py | 48 ++++++++++++++++++++++ tests/test_feature_flags.py | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 bugsnag/feature_flags.py create mode 100644 tests/test_feature_flags.py diff --git a/bugsnag/feature_flags.py b/bugsnag/feature_flags.py new file mode 100644 index 00000000..0afb7fde --- /dev/null +++ b/bugsnag/feature_flags.py @@ -0,0 +1,48 @@ +from typing import Dict, Union + + +class FeatureFlag: + def __init__( + self, + name: Union[str, bytes], + variant: Union[None, str, bytes] = None + ): + self._name = name + self._variant = self._coerce_variant(variant) + + @property + def name(self) -> Union[str, bytes]: + return self._name + + @property + def variant(self) -> Union[None, str, bytes]: + return self._variant + + # for JSON encoding the feature flag + def to_dict(self) -> Dict[str, Union[str, bytes]]: + if self._variant is None: + return {'name': self._name} + + return {'name': self._name, 'variant': self._variant} + + # a FeatureFlag is valid if it has a non-empty string name and a variant + # that's None or a string + # FeatureFlags that are not valid will be ignored + def is_valid(self) -> bool: + return ( + isinstance(self._name, (str, bytes)) and + len(self._name) > 0 and + (self._variant is None or isinstance(self._variant, (str, bytes))) + ) + + def _coerce_variant( + self, + variant: Union[None, str, bytes] + ) -> Union[None, str, bytes]: + if variant is None or isinstance(variant, (str, bytes)): + return variant + + try: + return str(variant) + except Exception: + return None diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py new file mode 100644 index 00000000..028b91be --- /dev/null +++ b/tests/test_feature_flags.py @@ -0,0 +1,80 @@ +from bugsnag.feature_flags import FeatureFlag + + +class Unstringable: + def __str__(self): + raise Exception('no') + + def __repr__(self): + raise Exception('nope') + + +def test_feature_flag_has_name_and_variant(): + flag = FeatureFlag('abc', 'xyz') + + assert flag.name == 'abc' + assert flag.variant == 'xyz' + + +def test_feature_flag_variant_is_optional(): + assert FeatureFlag('a').variant is None + assert FeatureFlag('a', None).variant is None + + +def test_feature_flag_variant_is_coerced_to_string(): + assert FeatureFlag('a', 123).variant == '123' + assert FeatureFlag('a', [1, 2, 3]).variant == '[1, 2, 3]' + + +def test_feature_flag_name_and_variant_can_be_bytes(): + flag = FeatureFlag(b'abc', b'xyz') + + assert flag.name == b'abc' + assert flag.variant == b'xyz' + + +def test_feature_flag_variant_is_unset_if_not_coercable(): + assert FeatureFlag('a', Unstringable()).variant is None + + +def test_feature_flag_can_be_converted_to_dict(): + flag = FeatureFlag('abc', 'xyz') + + assert flag.to_dict() == {'name': 'abc', 'variant': 'xyz'} + + +def test_feature_flag_dict_does_not_have_variant_when_variant_is_not_given(): + flag = FeatureFlag('xyz') + + assert flag.to_dict() == {'name': 'xyz'} + + +def test_feature_flag_dict_does_not_have_variant_when_variant_is_none(): + flag = FeatureFlag('abc', variant=None) + + assert flag.to_dict() == {'name': 'abc'} + + +def test_a_feature_flag_with_name_and_variant_is_valid(): + assert FeatureFlag('abc', 'xyz').is_valid() is True + assert FeatureFlag('abc', b'xyz').is_valid() is True + assert FeatureFlag(b'abc', b'xyz').is_valid() is True + assert FeatureFlag(b'abc', 'xyz').is_valid() is True + + +def test_a_feature_flag_with_only_name_is_valid(): + flag = FeatureFlag('b') + + assert flag.is_valid() is True + + +def test_a_feature_flag_with_empty_name_is_not_valid(): + flag = FeatureFlag('') + + assert flag.is_valid() is False + + +def test_a_feature_flag_with_none_name_is_not_valid(): + flag = FeatureFlag(None) + + assert flag.is_valid() is False