Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit edd138b

Browse files
feat: address PR comments
1 parent feac940 commit edd138b

3 files changed

Lines changed: 124 additions & 102 deletions

File tree

src/uipath/core/feature_flags/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
Local-only feature flag registry for the UiPath SDK.
44
"""
55

6-
from .feature_flags import configure, get, is_enabled, reset
6+
from .feature_flags import FeatureFlags, FeatureFlagsManager
77

88
__all__ = [
9-
"configure",
10-
"get",
11-
"is_enabled",
12-
"reset",
9+
"FeatureFlags",
10+
"FeatureFlagsManager",
1311
]
Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
"""Feature flags configuration for UiPath SDK.
22
33
A simple, local-only feature flag registry. Flags can be set
4-
programmatically via :func:`configure` or overridden per-flag with
5-
environment variables named ``UIPATH_FEATURE_<FlagName>``.
4+
programmatically via :meth:`FeatureFlagsManager.configure_flags` or
5+
supplied via environment variables named ``UIPATH_FEATURE_<FlagName>``
6+
when nothing has been configured programmatically.
67
7-
Environment variables always take precedence over programmatic values.
8+
Programmatic values always take precedence over environment variables.
89
910
Example usage::
1011
11-
from uipath.core.feature_flags import configure, is_enabled, get
12+
from uipath.core.feature_flags import FeatureFlags
1213
1314
# Programmatic configuration (e.g. from an upstream layer)
14-
configure({"NewSerialization": True, "ModelOverride": "gpt-4"})
15+
FeatureFlags.configure_flags({"NewSerialization": True, "ModelOverride": "gpt-4"})
1516
1617
# Check a boolean flag
17-
if is_enabled("NewSerialization"):
18+
if FeatureFlags.is_flag_enabled("NewSerialization"):
1819
...
1920
2021
# Get an arbitrary value
21-
model = get("ModelOverride", default="default-model")
22+
model = FeatureFlags.get_flag("ModelOverride", default="default-model")
2223
2324
# Local override via environment variable
2425
# $ export UIPATH_FEATURE_NewSerialization=false
@@ -28,23 +29,6 @@
2829
import os
2930
from typing import Any
3031

31-
_flags: dict[str, Any] = {}
32-
33-
34-
def configure(flags: dict[str, Any]) -> None:
35-
"""Merge feature flag values into the registry.
36-
37-
Args:
38-
flags: Mapping of flag names to their values. Existing flags
39-
with the same name are overwritten.
40-
"""
41-
_flags.update(flags)
42-
43-
44-
def reset() -> None:
45-
"""Clear all configured flags. Mainly useful in tests."""
46-
_flags.clear()
47-
4832

4933
def _parse_env_value(raw: str) -> Any:
5034
"""Convert an environment variable string to a Python value.
@@ -69,32 +53,65 @@ def _parse_env_value(raw: str) -> Any:
6953
return raw
7054

7155

72-
def get(name: str, *, default: Any = None) -> Any:
73-
"""Return a flag value.
56+
class FeatureFlagsManager:
57+
"""Singleton registry for UiPath feature flags.
7458
75-
Resolution order:
59+
Use the module-level :data:`FeatureFlags` instance rather than
60+
instantiating this class directly.
61+
"""
7662

77-
1. ``UIPATH_FEATURE_<name>`` environment variable (highest priority)
78-
2. Value set via :func:`configure`
79-
3. *default*
63+
_instance: "FeatureFlagsManager | None" = None
64+
_flags: dict[str, Any]
8065

81-
Args:
82-
name: The feature flag name.
83-
default: Fallback when the flag is not set anywhere.
84-
"""
85-
env_val = os.environ.get(f"UIPATH_FEATURE_{name}")
86-
if env_val is not None:
87-
return _parse_env_value(env_val)
88-
return _flags.get(name, default)
66+
def __new__(cls) -> "FeatureFlagsManager":
67+
if cls._instance is None:
68+
cls._instance = super().__new__(cls)
69+
cls._instance._flags = {}
70+
return cls._instance
8971

72+
def configure_flags(self, flags: dict[str, Any]) -> None:
73+
"""Merge feature flag values into the registry.
9074
91-
def is_enabled(name: str, *, default: bool = False) -> bool:
92-
"""Check whether a boolean flag is enabled.
75+
Args:
76+
flags: Mapping of flag names to their values. Existing flags
77+
with the same name are overwritten.
78+
"""
79+
self._flags.update(flags)
9380

94-
Uses the same resolution order as :func:`get`.
81+
def reset_flags(self) -> None:
82+
"""Clear all configured flags."""
83+
self._flags.clear()
9584

96-
Args:
97-
name: The feature flag name.
98-
default: Fallback when the flag is not set anywhere.
99-
"""
100-
return bool(get(name, default=default))
85+
def get_flag(self, name: str, *, default: Any = None) -> Any:
86+
"""Return a flag value.
87+
88+
Resolution order:
89+
90+
1. Value set via :meth:`configure_flags` (highest priority)
91+
2. ``UIPATH_FEATURE_<name>`` environment variable (fallback when nothing configured)
92+
3. *default*
93+
94+
Args:
95+
name: The feature flag name.
96+
default: Fallback when the flag is not set anywhere.
97+
"""
98+
if name in self._flags:
99+
return self._flags[name]
100+
env_val = os.environ.get(f"UIPATH_FEATURE_{name}")
101+
if env_val is not None:
102+
return _parse_env_value(env_val)
103+
return default
104+
105+
def is_flag_enabled(self, name: str, *, default: bool = False) -> bool:
106+
"""Check whether a boolean flag is enabled.
107+
108+
Uses the same resolution order as :meth:`get_flag`.
109+
110+
Args:
111+
name: The feature flag name.
112+
default: Fallback when the flag is not set anywhere.
113+
"""
114+
return bool(self.get_flag(name, default=default))
115+
116+
117+
FeatureFlags = FeatureFlagsManager()

tests/feature_flags/test_feature_flags.py

Lines changed: 58 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import TYPE_CHECKING
44

5-
from uipath.core.feature_flags import configure, get, is_enabled, reset
5+
from uipath.core.feature_flags import FeatureFlags
66
from uipath.core.feature_flags.feature_flags import _parse_env_value
77

88
if TYPE_CHECKING:
@@ -55,106 +55,113 @@ def test_plain_string_not_json(self) -> None:
5555
assert _parse_env_value("gpt-4") == "gpt-4"
5656

5757

58-
class TestConfigure:
59-
"""Tests for configure / reset."""
58+
class TestConfigureFlags:
59+
"""Tests for configure_flags / reset_flags."""
6060

6161
def setup_method(self) -> None:
62-
reset()
62+
FeatureFlags.reset_flags()
6363

6464
def test_configure_sets_flags(self) -> None:
65-
configure({"FeatureA": True, "FeatureB": "value"})
66-
assert get("FeatureA") is True
67-
assert get("FeatureB") == "value"
65+
FeatureFlags.configure_flags({"FeatureA": True, "FeatureB": "value"})
66+
assert FeatureFlags.get_flag("FeatureA") is True
67+
assert FeatureFlags.get_flag("FeatureB") == "value"
6868

6969
def test_configure_merges(self) -> None:
70-
configure({"FeatureA": True})
71-
configure({"FeatureB": False})
72-
assert get("FeatureA") is True
73-
assert get("FeatureB") is False
70+
FeatureFlags.configure_flags({"FeatureA": True})
71+
FeatureFlags.configure_flags({"FeatureB": False})
72+
assert FeatureFlags.get_flag("FeatureA") is True
73+
assert FeatureFlags.get_flag("FeatureB") is False
7474

7575
def test_configure_overwrites(self) -> None:
76-
configure({"FeatureA": True})
77-
configure({"FeatureA": False})
78-
assert get("FeatureA") is False
76+
FeatureFlags.configure_flags({"FeatureA": True})
77+
FeatureFlags.configure_flags({"FeatureA": False})
78+
assert FeatureFlags.get_flag("FeatureA") is False
7979

8080
def test_reset_clears_all(self) -> None:
81-
configure({"FeatureA": True})
82-
reset()
83-
assert get("FeatureA") is None
81+
FeatureFlags.configure_flags({"FeatureA": True})
82+
FeatureFlags.reset_flags()
83+
assert FeatureFlags.get_flag("FeatureA") is None
8484

8585

86-
class TestGet:
87-
"""Tests for get."""
86+
class TestGetFlag:
87+
"""Tests for get_flag."""
8888

8989
def setup_method(self) -> None:
90-
reset()
90+
FeatureFlags.reset_flags()
9191

9292
def test_returns_default_when_unset(self) -> None:
93-
assert get("Missing") is None
93+
assert FeatureFlags.get_flag("Missing") is None
9494

9595
def test_returns_custom_default(self) -> None:
96-
assert get("Missing", default="fallback") == "fallback"
96+
assert FeatureFlags.get_flag("Missing", default="fallback") == "fallback"
9797

9898
def test_returns_configured_value(self) -> None:
99-
configure({"FeatureA": "hello"})
100-
assert get("FeatureA") == "hello"
99+
FeatureFlags.configure_flags({"FeatureA": "hello"})
100+
assert FeatureFlags.get_flag("FeatureA") == "hello"
101101

102-
def test_env_var_overrides_configured(self, monkeypatch: "MonkeyPatch") -> None:
103-
configure({"FeatureA": True})
102+
def test_configured_value_takes_precedence_over_env_var(
103+
self, monkeypatch: "MonkeyPatch"
104+
) -> None:
105+
FeatureFlags.configure_flags({"FeatureA": True})
104106
monkeypatch.setenv("UIPATH_FEATURE_FeatureA", "false")
105-
assert get("FeatureA") is False
107+
assert FeatureFlags.get_flag("FeatureA") is True
106108

107-
def test_env_var_overrides_default(self, monkeypatch: "MonkeyPatch") -> None:
109+
def test_env_var_used_when_nothing_configured(
110+
self, monkeypatch: "MonkeyPatch"
111+
) -> None:
108112
monkeypatch.setenv("UIPATH_FEATURE_X", "custom")
109-
assert get("X", default="other") == "custom"
113+
assert FeatureFlags.get_flag("X", default="other") == "custom"
110114

111115
def test_env_var_string_value(self, monkeypatch: "MonkeyPatch") -> None:
112116
monkeypatch.setenv("UIPATH_FEATURE_Model", "gpt-4-turbo")
113-
assert get("Model") == "gpt-4-turbo"
117+
assert FeatureFlags.get_flag("Model") == "gpt-4-turbo"
114118

115119
def test_env_var_json_dict(self, monkeypatch: "MonkeyPatch") -> None:
116120
monkeypatch.setenv("UIPATH_FEATURE_Models", '{"gpt-4": true, "claude": false}')
117-
assert get("Models") == {"gpt-4": True, "claude": False}
121+
assert FeatureFlags.get_flag("Models") == {"gpt-4": True, "claude": False}
118122

119123
def test_env_var_json_list(self, monkeypatch: "MonkeyPatch") -> None:
120124
monkeypatch.setenv("UIPATH_FEATURE_AllowedModels", '["gpt-4", "claude"]')
121-
assert get("AllowedModels") == ["gpt-4", "claude"]
125+
assert FeatureFlags.get_flag("AllowedModels") == ["gpt-4", "claude"]
122126

123127

124-
class TestIsEnabled:
125-
"""Tests for is_enabled."""
128+
class TestIsFlagEnabled:
129+
"""Tests for is_flag_enabled."""
126130

127131
def setup_method(self) -> None:
128-
reset()
132+
FeatureFlags.reset_flags()
129133

130134
def test_enabled_flag(self) -> None:
131-
configure({"FeatureA": True})
132-
assert is_enabled("FeatureA") is True
135+
FeatureFlags.configure_flags({"FeatureA": True})
136+
assert FeatureFlags.is_flag_enabled("FeatureA") is True
133137

134138
def test_disabled_flag(self) -> None:
135-
configure({"FeatureA": False})
136-
assert is_enabled("FeatureA") is False
139+
FeatureFlags.configure_flags({"FeatureA": False})
140+
assert FeatureFlags.is_flag_enabled("FeatureA") is False
137141

138142
def test_missing_flag_defaults_false(self) -> None:
139-
assert is_enabled("Missing") is False
143+
assert FeatureFlags.is_flag_enabled("Missing") is False
140144

141145
def test_missing_flag_custom_default(self) -> None:
142-
assert is_enabled("Missing", default=True) is True
146+
assert FeatureFlags.is_flag_enabled("Missing", default=True) is True
143147

144148
def test_truthy_string_is_enabled(self) -> None:
145-
configure({"FeatureA": "some-value"})
146-
assert is_enabled("FeatureA") is True
149+
FeatureFlags.configure_flags({"FeatureA": "some-value"})
150+
assert FeatureFlags.is_flag_enabled("FeatureA") is True
147151

148152
def test_none_is_disabled(self) -> None:
149-
configure({"FeatureA": None})
150-
assert is_enabled("FeatureA") is False
153+
FeatureFlags.configure_flags({"FeatureA": None})
154+
assert FeatureFlags.is_flag_enabled("FeatureA") is False
151155

152-
def test_env_override_disables(self, monkeypatch: "MonkeyPatch") -> None:
153-
configure({"FeatureA": True})
156+
def test_configured_value_takes_precedence_over_env_var(
157+
self, monkeypatch: "MonkeyPatch"
158+
) -> None:
159+
FeatureFlags.configure_flags({"FeatureA": True})
154160
monkeypatch.setenv("UIPATH_FEATURE_FeatureA", "false")
155-
assert is_enabled("FeatureA") is False
161+
assert FeatureFlags.is_flag_enabled("FeatureA") is True
156162

157-
def test_env_override_enables(self, monkeypatch: "MonkeyPatch") -> None:
158-
configure({"FeatureA": False})
163+
def test_env_var_used_when_nothing_configured(
164+
self, monkeypatch: "MonkeyPatch"
165+
) -> None:
159166
monkeypatch.setenv("UIPATH_FEATURE_FeatureA", "true")
160-
assert is_enabled("FeatureA") is True
167+
assert FeatureFlags.is_flag_enabled("FeatureA") is True

0 commit comments

Comments
 (0)