Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for basic feature flags #35

Merged
merged 6 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions dissect/util/feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import functools
import os
from enum import Enum
from typing import Callable, Optional


# Register feature flags in a central place to avoid chaos
class Feature(Enum):
cecinestpasunepipe marked this conversation as resolved.
Show resolved Hide resolved
LATEST = "latest"
ADVANCED = "advanced"
BETA = "beta"


# Defines the default flags (as strings)
DISSECT_FEATURES_DEFAULT = "latest"

# Defines the environment variable to read the flags from
DISSECT_FEATURES_ENV = "DISSECT_FEATURES"


class FeatureException(RuntimeError):
pass


@functools.cache
def feature_flags() -> list[Feature]:
return [Feature(name) for name in os.getenv(DISSECT_FEATURES_ENV, DISSECT_FEATURES_DEFAULT).split("/")]


@functools.cache
def feature_enabled(feature: Feature) -> bool:
cecinestpasunepipe marked this conversation as resolved.
Show resolved Hide resolved
"""Use this function for block-level feature flag control.

Usage::

def parse_blob():
if feature_enabled(Feature.BETA):
self._parse_fast_experimental()
else:
self._parse_normal()

"""
cecinestpasunepipe marked this conversation as resolved.
Show resolved Hide resolved
return feature in feature_flags()


def feature(flag: Feature, alternative: Optional[Callable] = None) -> Callable:
"""Feature flag decorator allowing you to guard a function behind a feature flag.

Usage::

@feature(Feature.SOME_FLAG, fallback)
def my_func( ... ) -> ...

Where ``SOME_FLAG`` is the feature you want to check for and ``fallback`` is the alternative function to serve
if the feature flag is NOT set.
"""

if alternative is None:

def alternative():
raise FeatureException(
"\n".join(
[
"Feature disabled.",
f"Set FLAG '{flag}' in {DISSECT_FEATURES_ENV} to enable.",
"See https://docs.dissect.tools/en/latest/advanced/flags.html",
]
)
)

def decorator(func):
return func if feature_enabled(flag) else alternative

return decorator
35 changes: 35 additions & 0 deletions tests/test_feature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import pytest

from dissect.util.feature import Feature, FeatureException, feature, feature_enabled


def test_feature_flags() -> None:
def fallback():
return False

@feature(Feature.BETA, fallback)
def experimental():
return True

@feature(Feature.ADVANCED, fallback)
def advanced():
return True

@feature(Feature.LATEST)
def latest():
return True

@feature("expert")
def expert():
return True

assert experimental() is False
assert advanced() is False
assert latest() is True
with pytest.raises(FeatureException):
assert expert() is True


def test_feature_flag_inline() -> None:
assert feature_enabled(Feature.BETA) is False
assert feature_enabled(Feature.LATEST) is True