Skip to content

Commit 29c75aa

Browse files
committed
feat(cli): add CliRuntime.reload_project_config for post-init overrides
Adds a method that rebuilds project_config with an additional_yaml kwarg and re-binds the keychain. Needed because CliRuntime is constructed in cci.main() before Click has parsed subcommand options, so flags like --extra-yaml cannot be applied at construction time.
1 parent 470c1e9 commit 29c75aa

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

cumulusci/cli/runtime.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
from logging import getLogger
55
from subprocess import call
6+
from typing import Optional
67

78
import click
89
import keyring
@@ -26,6 +27,26 @@ def __init__(self, *args, **kwargs):
2627
except (KeychainKeyNotFound) as e:
2728
raise click.UsageError(f"Keychain Error: {str(e)}")
2829

30+
def reload_project_config(self, additional_yaml: Optional[str] = None) -> None:
31+
"""Rebuild project_config with an ``additional_yaml`` override.
32+
33+
``CliRuntime`` loads ``project_config`` at construction time (via
34+
``BaseCumulusCI.__init__``), but Click options are only known after
35+
construction. This method rebuilds ``project_config`` with the given
36+
``additional_yaml`` string and re-binds the keychain if one is
37+
attached. No-ops when ``additional_yaml`` is ``None``.
38+
"""
39+
if additional_yaml is None:
40+
return
41+
try:
42+
self._load_project_config(additional_yaml=additional_yaml)
43+
except ConfigError as e:
44+
raise click.UsageError(f"Config Error: {str(e)}")
45+
if self.keychain is not None:
46+
self.keychain.project_config = self.project_config
47+
if self.project_config is not None:
48+
self.project_config.keychain = self.keychain
49+
2950
def get_keychain_class(self):
3051
default_keychain_class = (
3152
self.project_config.cumulusci__keychain

cumulusci/cli/tests/test_runtime.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,51 @@ def test_get_org_missing(self):
107107
with pytest.raises(click.UsageError):
108108
org_name, org_config_result = config.get_org("test", fail_if_missing=True)
109109

110+
def test_reload_project_config__applies_additional_yaml(self):
111+
config = CliRuntime()
112+
assert config.project_config.config_additional_yaml == {}
113+
114+
config.reload_project_config(
115+
additional_yaml=(
116+
"tasks:\n"
117+
" injected:\n"
118+
" description: via reload\n"
119+
" class_path: cumulusci.tasks.util.Sleep\n"
120+
)
121+
)
122+
123+
assert config.project_config.config_additional_yaml != {}
124+
assert config.project_config.tasks["injected"]["description"] == "via reload"
125+
126+
def test_reload_project_config__rebinds_keychain(self):
127+
config = CliRuntime()
128+
old_keychain = config.keychain
129+
assert old_keychain is not None
130+
old_project_config = config.project_config
131+
132+
config.reload_project_config(
133+
additional_yaml="project:\n custom:\n reloaded: true\n"
134+
)
135+
136+
assert config.keychain is old_keychain
137+
assert config.project_config is not old_project_config
138+
assert config.keychain.project_config is config.project_config
139+
assert config.project_config.keychain is config.keychain
140+
141+
def test_reload_project_config__noop_when_none(self):
142+
config = CliRuntime()
143+
before = config.project_config
144+
config.reload_project_config(additional_yaml=None)
145+
assert config.project_config is before
146+
147+
def test_reload_project_config__wraps_config_error(self):
148+
config = CliRuntime()
149+
150+
with mock.patch.object(
151+
config, "_load_project_config", side_effect=ConfigError("boom")
152+
), pytest.raises(click.UsageError, match="Config Error: boom"):
153+
config.reload_project_config(additional_yaml="project: {}\n")
154+
110155
def test_check_org_expired(self):
111156
config = CliRuntime()
112157
config.keychain = mock.Mock()

0 commit comments

Comments
 (0)