Skip to content

Commit d6f4fbd

Browse files
authored
feat: manage all beta features (#2322)
Signed-off-by: Matteo Fari <matteofari06@gmail.com>
1 parent 9b4dd8c commit d6f4fbd

5 files changed

Lines changed: 94 additions & 0 deletions

File tree

docs/tutorials/cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ trestle beta enable example-feature
202202
trestle beta disable example-feature
203203
```
204204

205+
Use `all` to enable every registered beta feature or disable every feature stored in the selected config. Features
206+
enabled by environment or by default remain enabled.
207+
208+
```bash
209+
trestle beta enable all
210+
trestle beta disable all
211+
```
212+
205213
Use `--beta` on a beta command to run it one time without writing beta state to config. Commands that are not beta
206214
features will warn that `--beta` is only effective for beta level commands, but will otherwise proceed normally.
207215

tests/trestle/core/beta_features_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ def test_enable_disable_feature_persists_to_workspace_config(
7676
assert not beta_features.disable_feature('sample', tmp_trestle_dir)
7777

7878

79+
def test_enable_disable_all_features_preserves_unknown_config(
80+
tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch
81+
) -> None:
82+
"""Test bulk beta updates preserve unregistered config entries."""
83+
patch_beta_features(monkeypatch, sample_feature('one'), sample_feature('two'))
84+
config_path = beta_features.get_beta_config_path(tmp_trestle_dir)
85+
config_path.write_text('[beta]\nenabled_features = legacy\n', encoding='utf-8')
86+
87+
assert beta_features.enable_all_features(tmp_trestle_dir) == 2
88+
assert beta_features.get_enabled_features(tmp_trestle_dir) == {'one', 'two'}
89+
assert beta_features.enable_all_features(tmp_trestle_dir) == 0
90+
91+
assert beta_features.disable_all_features(tmp_trestle_dir) == 2
92+
assert not beta_features.get_enabled_features(tmp_trestle_dir)
93+
assert beta_features.disable_all_features(tmp_trestle_dir) == 0
94+
95+
config = configparser.ConfigParser()
96+
config.read(config_path)
97+
assert config.get(beta_features.BETA_SECTION, beta_features.ENABLED_FEATURES_KEY) == 'legacy'
98+
99+
79100
def test_enable_feature_preserves_existing_config_sections(
80101
tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch
81102
) -> None:

tests/trestle/core/commands/beta_test.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,38 @@ def test_beta_enable_query_disable_feature(
9090
assert not beta_features.is_beta_enabled('sample', tmp_trestle_dir)
9191

9292

93+
def test_beta_enable_disable_all(
94+
tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]
95+
) -> None:
96+
"""Test enabling and disabling all registered beta features."""
97+
patch_beta_features(monkeypatch, sample_feature('one'), sample_feature('two'))
98+
99+
execute_command_and_assert('trestle beta enable all', CmdReturnCodes.SUCCESS.value, monkeypatch)
100+
output, _ = capsys.readouterr()
101+
assert 'Enabled 2 beta feature(s).' in output
102+
assert beta_features.get_enabled_features(tmp_trestle_dir) == {'one', 'two'}
103+
104+
execute_command_and_assert('trestle beta disable all', CmdReturnCodes.SUCCESS.value, monkeypatch)
105+
output, _ = capsys.readouterr()
106+
assert 'Disabled 2 beta feature(s).' in output
107+
assert not beta_features.get_enabled_features(tmp_trestle_dir)
108+
109+
110+
def test_beta_disable_all_reports_environment_enabled_features(
111+
tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]
112+
) -> None:
113+
"""Test disabling all reports features that config cannot disable."""
114+
patch_beta_features(monkeypatch, sample_feature())
115+
monkeypatch.setenv(beta_features.TRESTLE_BETA_FEATURES_ENV, 'sample')
116+
117+
execute_command_and_assert('trestle beta disable all', CmdReturnCodes.SUCCESS.value, monkeypatch)
118+
119+
output, _ = capsys.readouterr()
120+
assert 'Disabled 0 beta feature(s).' in output
121+
assert 'Features still enabled by environment or default: sample' in output
122+
assert beta_features.is_beta_enabled('sample', tmp_trestle_dir)
123+
124+
93125
def test_beta_query_verbose(monkeypatch: MonkeyPatch, capsys: CaptureFixture[str]) -> None:
94126
"""Test verbose beta query output."""
95127
patch_beta_features(monkeypatch, sample_feature())

trestle/core/beta_features.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,26 @@ def disable_feature(feature_name: str, trestle_root: pathlib.Path) -> bool:
163163
return True
164164

165165

166+
def enable_all_features(trestle_root: pathlib.Path) -> int:
167+
"""Enable every registered beta feature and return the number changed."""
168+
config_path = get_beta_config_path(trestle_root)
169+
enabled_features = _read_config_enabled_features(config_path)
170+
newly_enabled = set(BETA_FEATURES).difference(enabled_features)
171+
if newly_enabled:
172+
_write_config_enabled_features(config_path, enabled_features.union(newly_enabled))
173+
return len(newly_enabled)
174+
175+
176+
def disable_all_features(trestle_root: pathlib.Path) -> int:
177+
"""Disable every config-enabled beta feature and return the number changed."""
178+
config_path = get_beta_config_path(trestle_root)
179+
enabled_features = _read_config_enabled_features(config_path)
180+
configured_features = set(BETA_FEATURES).intersection(enabled_features)
181+
if configured_features:
182+
_write_config_enabled_features(config_path, enabled_features.difference(configured_features))
183+
return len(configured_features)
184+
185+
166186
def beta_feature(feature_name: str) -> Callable[[BetaCallable], BetaCallable]:
167187
"""Decorate a command run method so it requires an enabled beta feature."""
168188

trestle/core/commands/beta.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def _show_help(self) -> None:
8888
self.out(' trestle beta query List beta features and their status')
8989
self.out(' trestle beta enable <feature> Enable a beta feature')
9090
self.out(' trestle beta disable <feature> Disable a beta feature')
91+
self.out(' trestle beta enable all Enable all beta features')
92+
self.out(' trestle beta disable all Disable all config-enabled beta features')
9193
self.out('')
9294
self.out('Use "trestle beta query --verbose" for detailed descriptions.')
9395

@@ -151,6 +153,10 @@ def _enable_feature(self, feature_name: str, trestle_root: pathlib.Path) -> None
151153
"""Enable a beta feature."""
152154
if not feature_name:
153155
raise TrestleIncorrectArgsError('A beta feature name is required.')
156+
if feature_name == 'all':
157+
changed = beta_features.enable_all_features(trestle_root)
158+
self.out(f'Enabled {changed} beta feature(s).')
159+
return
154160

155161
feature = self._get_feature(feature_name)
156162
changed = beta_features.enable_feature(feature_name, trestle_root)
@@ -176,6 +182,13 @@ def _disable_feature(self, feature_name: str, trestle_root: pathlib.Path) -> Non
176182
"""Disable a beta feature."""
177183
if not feature_name:
178184
raise TrestleIncorrectArgsError('A beta feature name is required.')
185+
if feature_name == 'all':
186+
changed = beta_features.disable_all_features(trestle_root)
187+
self.out(f'Disabled {changed} beta feature(s).')
188+
remaining = beta_features.get_enabled_features(trestle_root)
189+
if remaining:
190+
self.out(f'Features still enabled by environment or default: {", ".join(sorted(remaining))}')
191+
return
179192

180193
feature = self._get_feature(feature_name)
181194
changed = beta_features.disable_feature(feature_name, trestle_root)

0 commit comments

Comments
 (0)