Skip to content

Commit

Permalink
Add FeatureFlag class
Browse files Browse the repository at this point in the history
  • Loading branch information
imjoehaines committed Jul 20, 2023
1 parent 955d088 commit a4a20ca
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 0 deletions.
48 changes: 48 additions & 0 deletions bugsnag/feature_flags.py
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions tests/test_feature_flags.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a4a20ca

Please sign in to comment.