Skip to content

Commit 53b67ce

Browse files
xuanyang15copybara-github
authored andcommitted
feat: Add --disable_features CLI option to ADK CLI
This flag can be used to override default feature enable state. Co-authored-by: Xuan Yang <xygoogle@google.com> PiperOrigin-RevId: 858659818
1 parent 21f63f6 commit 53b67ce

File tree

3 files changed

+179
-33
lines changed

3 files changed

+179
-33
lines changed

src/google/adk/cli/cli_tools_click.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,44 @@
5050
)
5151

5252

53-
def _apply_feature_overrides(enable_features: tuple[str, ...]) -> None:
53+
def _apply_feature_overrides(
54+
*,
55+
enable_features: tuple[str, ...] = (),
56+
disable_features: tuple[str, ...] = (),
57+
) -> None:
5458
"""Apply feature overrides from CLI flags.
5559
5660
Args:
5761
enable_features: Tuple of feature names to enable.
62+
disable_features: Tuple of feature names to disable.
5863
"""
64+
feature_overrides: dict[str, bool] = {}
65+
5966
for features_str in enable_features:
6067
for feature_name_str in features_str.split(","):
6168
feature_name_str = feature_name_str.strip()
62-
if not feature_name_str:
63-
continue
64-
try:
65-
feature_name = FeatureName(feature_name_str)
66-
override_feature_enabled(feature_name, True)
67-
except ValueError:
68-
valid_names = ", ".join(f.value for f in FeatureName)
69-
click.secho(
70-
f"WARNING: Unknown feature name '{feature_name_str}'. "
71-
f"Valid names are: {valid_names}",
72-
fg="yellow",
73-
err=True,
74-
)
69+
if feature_name_str:
70+
feature_overrides[feature_name_str] = True
71+
72+
for features_str in disable_features:
73+
for feature_name_str in features_str.split(","):
74+
feature_name_str = feature_name_str.strip()
75+
if feature_name_str:
76+
feature_overrides[feature_name_str] = False
77+
78+
# Apply all overrides
79+
for feature_name_str, enabled in feature_overrides.items():
80+
try:
81+
feature_name = FeatureName(feature_name_str)
82+
override_feature_enabled(feature_name, enabled)
83+
except ValueError:
84+
valid_names = ", ".join(f.value for f in FeatureName)
85+
click.secho(
86+
f"WARNING: Unknown feature name '{feature_name_str}'. "
87+
f"Valid names are: {valid_names}",
88+
fg="yellow",
89+
err=True,
90+
)
7591

7692

7793
def feature_options():
@@ -88,11 +104,25 @@ def decorator(func):
88104
),
89105
multiple=True,
90106
)
107+
@click.option(
108+
"--disable_features",
109+
help=(
110+
"Optional. Comma-separated list of feature names to disable. "
111+
"This provides an alternative to environment variables for "
112+
"disabling features. Example: "
113+
"--disable_features=JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING"
114+
),
115+
multiple=True,
116+
)
91117
@functools.wraps(func)
92118
def wrapper(*args, **kwargs):
93119
enable_features = kwargs.pop("enable_features", ())
94-
if enable_features:
95-
_apply_feature_overrides(enable_features)
120+
disable_features = kwargs.pop("disable_features", ())
121+
if enable_features or disable_features:
122+
_apply_feature_overrides(
123+
enable_features=enable_features,
124+
disable_features=disable_features,
125+
)
96126
return func(*args, **kwargs)
97127

98128
return wrapper

tests/unittests/cli/test_cli_feature_options.py

Lines changed: 123 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Unit tests for --enable_features CLI option."""
16-
1715
from __future__ import annotations
1816

1917
import click
@@ -42,45 +40,96 @@ class TestApplyFeatureOverrides:
4240

4341
def test_single_feature(self):
4442
"""Single feature name is applied correctly."""
45-
_apply_feature_overrides(("JSON_SCHEMA_FOR_FUNC_DECL",))
43+
_apply_feature_overrides(enable_features=("JSON_SCHEMA_FOR_FUNC_DECL",))
4644
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
4745

4846
def test_comma_separated_features(self):
4947
"""Comma-separated feature names are applied correctly."""
50-
_apply_feature_overrides((
51-
"JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING",
52-
))
48+
_apply_feature_overrides(
49+
enable_features=("JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING",)
50+
)
5351
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
5452
assert is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING)
5553

5654
def test_multiple_flag_values(self):
5755
"""Multiple --enable_features flags are applied correctly."""
58-
_apply_feature_overrides((
59-
"JSON_SCHEMA_FOR_FUNC_DECL",
60-
"PROGRESSIVE_SSE_STREAMING",
61-
))
56+
_apply_feature_overrides(
57+
enable_features=(
58+
"JSON_SCHEMA_FOR_FUNC_DECL",
59+
"PROGRESSIVE_SSE_STREAMING",
60+
)
61+
)
6262
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
6363
assert is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING)
6464

6565
def test_whitespace_handling(self):
6666
"""Whitespace around feature names is stripped."""
67-
_apply_feature_overrides((" JSON_SCHEMA_FOR_FUNC_DECL , COMPUTER_USE ",))
67+
_apply_feature_overrides(
68+
enable_features=(" JSON_SCHEMA_FOR_FUNC_DECL , COMPUTER_USE ",)
69+
)
6870
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
6971
assert is_feature_enabled(FeatureName.COMPUTER_USE)
7072

7173
def test_empty_string_ignored(self):
7274
"""Empty strings in the list are ignored."""
73-
_apply_feature_overrides(("",))
75+
_apply_feature_overrides(enable_features=("",))
7476
# No error should be raised
7577

7678
def test_unknown_feature_warns(self, capsys):
7779
"""Unknown feature names emit a warning."""
78-
_apply_feature_overrides(("UNKNOWN_FEATURE_XYZ",))
80+
_apply_feature_overrides(enable_features=("UNKNOWN_FEATURE_XYZ",))
7981
captured = capsys.readouterr()
8082
assert "WARNING" in captured.err
8183
assert "UNKNOWN_FEATURE_XYZ" in captured.err
8284
assert "Valid names are:" in captured.err
8385

86+
def test_single_disable_feature(self):
87+
"""Single feature name is disabled correctly."""
88+
# First enable a feature
89+
_apply_feature_overrides(enable_features=("JSON_SCHEMA_FOR_FUNC_DECL",))
90+
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
91+
92+
# Then disable it
93+
_apply_feature_overrides(disable_features=("JSON_SCHEMA_FOR_FUNC_DECL",))
94+
assert not is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
95+
96+
def test_comma_separated_disable_features(self):
97+
"""Comma-separated feature names are disabled correctly."""
98+
# First enable features
99+
_apply_feature_overrides(
100+
enable_features=("JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING",)
101+
)
102+
103+
# Then disable them
104+
_apply_feature_overrides(
105+
disable_features=(
106+
"JSON_SCHEMA_FOR_FUNC_DECL,PROGRESSIVE_SSE_STREAMING",
107+
)
108+
)
109+
assert not is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
110+
assert not is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING)
111+
112+
def test_disable_overrides_enable(self):
113+
"""Disable is applied after enable, so disable wins for same feature."""
114+
_apply_feature_overrides(
115+
enable_features=("JSON_SCHEMA_FOR_FUNC_DECL",),
116+
disable_features=("JSON_SCHEMA_FOR_FUNC_DECL",),
117+
)
118+
# disable_features is processed after enable_features
119+
assert not is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
120+
121+
def test_enable_and_disable_different_features(self):
122+
"""Enable and disable can be used together for different features."""
123+
# First enable a feature that we'll disable
124+
_apply_feature_overrides(enable_features=("PROGRESSIVE_SSE_STREAMING",))
125+
126+
_apply_feature_overrides(
127+
enable_features=("JSON_SCHEMA_FOR_FUNC_DECL",),
128+
disable_features=("PROGRESSIVE_SSE_STREAMING",),
129+
)
130+
assert is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
131+
assert not is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING)
132+
84133

85134
class TestFeatureOptionsDecorator:
86135
"""Tests for feature_options decorator."""
@@ -195,3 +244,64 @@ def my_test_command():
195244
"my_test_command" in my_test_command.name
196245
or my_test_command.callback.__name__ == "my_test_command"
197246
)
247+
248+
def test_decorator_adds_disable_features_option(self):
249+
"""Decorator adds --disable_features option to command."""
250+
251+
@click.command()
252+
@feature_options()
253+
def test_cmd():
254+
pass
255+
256+
runner = CliRunner()
257+
result = runner.invoke(test_cmd, ["--help"])
258+
assert "--disable_features" in result.output
259+
260+
def test_disable_features_applied_before_command(self):
261+
"""Features are disabled before the command function runs."""
262+
# First enable the feature via override
263+
_apply_feature_overrides(enable_features=("JSON_SCHEMA_FOR_FUNC_DECL",))
264+
265+
feature_was_disabled = []
266+
267+
@click.command()
268+
@feature_options()
269+
def test_cmd():
270+
feature_was_disabled.append(
271+
not is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
272+
)
273+
274+
runner = CliRunner()
275+
runner.invoke(
276+
test_cmd,
277+
["--disable_features=JSON_SCHEMA_FOR_FUNC_DECL"],
278+
catch_exceptions=False,
279+
)
280+
assert feature_was_disabled == [True]
281+
282+
def test_enable_and_disable_together(self):
283+
"""Both --enable_features and --disable_features work together."""
284+
feature_states = []
285+
286+
@click.command()
287+
@feature_options()
288+
def test_cmd():
289+
feature_states.append(
290+
is_feature_enabled(FeatureName.JSON_SCHEMA_FOR_FUNC_DECL)
291+
)
292+
feature_states.append(
293+
is_feature_enabled(FeatureName.PROGRESSIVE_SSE_STREAMING)
294+
)
295+
296+
runner = CliRunner()
297+
runner.invoke(
298+
test_cmd,
299+
[
300+
"--enable_features=JSON_SCHEMA_FOR_FUNC_DECL",
301+
"--disable_features=PROGRESSIVE_SSE_STREAMING",
302+
],
303+
catch_exceptions=False,
304+
)
305+
# JSON_SCHEMA_FOR_FUNC_DECL should be enabled
306+
# PROGRESSIVE_SSE_STREAMING should be disabled
307+
assert feature_states == [True, False]

tests/unittests/cli/test_cli_tools_click_option_mismatch.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ def test_adk_run():
9595

9696
assert run_command is not None, "Run command not found"
9797
_check_options_in_parameters(
98-
run_command, cli_run.callback, "run", ignore_params={"enable_features"}
98+
run_command,
99+
cli_run.callback,
100+
"run",
101+
ignore_params={"enable_features", "disable_features"},
99102
)
100103

101104

@@ -105,7 +108,10 @@ def test_adk_eval():
105108

106109
assert eval_command is not None, "Eval command not found"
107110
_check_options_in_parameters(
108-
eval_command, cli_eval.callback, "eval", ignore_params={"enable_features"}
111+
eval_command,
112+
cli_eval.callback,
113+
"eval",
114+
ignore_params={"enable_features", "disable_features"},
109115
)
110116

111117

@@ -118,7 +124,7 @@ def test_adk_web():
118124
web_command,
119125
cli_web.callback,
120126
"web",
121-
ignore_params={"verbose", "enable_features"},
127+
ignore_params={"verbose", "enable_features", "disable_features"},
122128
)
123129

124130

@@ -131,7 +137,7 @@ def test_adk_api_server():
131137
api_server_command,
132138
cli_api_server.callback,
133139
"api_server",
134-
ignore_params={"verbose", "enable_features"},
140+
ignore_params={"verbose", "enable_features", "disable_features"},
135141
)
136142

137143

0 commit comments

Comments
 (0)