forked from ni/nidaqmx-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_feature_toggles.py
More file actions
156 lines (115 loc) · 4.7 KB
/
_feature_toggles.py
File metadata and controls
156 lines (115 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""nidaqmx feature toggles."""
# mypy: no-warn-unreachable
from __future__ import annotations
import functools
from collections.abc import Callable
from enum import Enum
from typing import TYPE_CHECKING, TypeVar
from decouple import AutoConfig, Undefined, undefined
from nidaqmx._dotenv_path import get_dotenv_search_path
from nidaqmx.errors import FeatureNotSupportedError
if TYPE_CHECKING:
from typing_extensions import ParamSpec, Self
_P = ParamSpec("_P")
_T = TypeVar("_T")
_PREFIX = "NIDAQMX"
if TYPE_CHECKING:
# Work around decouple's lack of type hints.
def _config(
option: str,
default: _T | Undefined = undefined,
cast: Callable[[str], _T] | Undefined = undefined,
) -> _T: ...
else:
_config = AutoConfig(str(get_dotenv_search_path()))
# Based on the recipe at https://docs.python.org/3/howto/enum.html
class _OrderedEnum(Enum):
def __ge__(self, other: Self) -> bool:
if self.__class__ is other.__class__:
return self.value >= other.value
return NotImplemented
def __gt__(self, other: Self) -> bool:
if self.__class__ is other.__class__:
return self.value > other.value
return NotImplemented
def __le__(self, other: Self) -> bool:
if self.__class__ is other.__class__:
return self.value <= other.value
return NotImplemented
def __lt__(self, other: Self) -> bool:
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
class CodeReadiness(_OrderedEnum):
"""Indicates whether code is ready to be supported."""
RELEASE = 0
NEXT_RELEASE = 1
INCOMPLETE = 2
PROTOTYPE = 3
def _init_code_readiness_level() -> CodeReadiness:
if _config(f"{_PREFIX}_ALLOW_INCOMPLETE", default=False, cast=bool):
return CodeReadiness.INCOMPLETE
elif _config(f"{_PREFIX}_ALLOW_NEXT_RELEASE", default=False, cast=bool):
return CodeReadiness.NEXT_RELEASE
else:
return CodeReadiness.RELEASE
# This is not public because `from _feature_toggles import CODE_READINESS_LEVEL`
# is incompatible with the patching performed by the use_code_readiness mark.
_CODE_READINESS_LEVEL = _init_code_readiness_level()
def get_code_readiness_level() -> CodeReadiness:
"""Get the current code readiness level.
You can override this in tests by specifying the ``use_code_readiness``
mark.
"""
return _CODE_READINESS_LEVEL
class FeatureToggle:
"""A run-time feature toggle."""
name: str
"""The name of the feature."""
readiness: CodeReadiness
"""The code readiness at which this feature is enabled."""
def __init__(self, name: str, readiness: CodeReadiness) -> None:
"""Initialize the feature toggle."""
assert name == name.upper()
self.name = name
self.readiness = readiness
self._is_enabled_override = None
# Only read the env var at initialization time.
if _config(f"{_PREFIX}_ENABLE_{name}", default=False, cast=bool):
self._is_enabled_override = True
@property
def is_enabled(self) -> bool:
"""Indicates whether the feature is currently enabled.
You can enable/disable features in tests by specifying the
``enable_feature_toggle`` or ``disable_feature_toggle`` marks.
"""
if self._is_enabled_override is not None:
return self._is_enabled_override
return self.readiness <= get_code_readiness_level()
def raise_if_disabled(self) -> None:
"""Raises an error if the feature is disabled."""
if self.is_enabled:
return
env_vars = f"{_PREFIX}_ENABLE_{self.name}"
if self.readiness in [CodeReadiness.NEXT_RELEASE, CodeReadiness.INCOMPLETE]:
env_vars += f" or {_PREFIX}_ALLOW_{self.readiness.name}"
message = (
f"The {self.name} feature is not supported at the current code readiness level. "
f" To enable it, set {env_vars}."
)
raise FeatureNotSupportedError(message)
def requires_feature(
feature_toggle: FeatureToggle,
) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]:
"""Decorator specifying that the function requires the specified feature toggle."""
def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]:
@functools.wraps(func)
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
feature_toggle.raise_if_disabled()
return func(*args, **kwargs)
return wrapper
return decorator
# --------------------------------------
# Define feature toggle constants here:
# --------------------------------------
WAVEFORM_SUPPORT = FeatureToggle("WAVEFORM_SUPPORT", CodeReadiness.RELEASE)