From d71b994e3176bae01c8b2657f94c21590e00c027 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Thu, 13 Feb 2025 11:12:48 -0800 Subject: [PATCH 1/2] CG-10801: support --global flag in config cmds --- src/codegen/cli/commands/config/main.py | 21 +++++--- src/codegen/shared/configs/constants.py | 1 + .../{global_session.py => global_config.py} | 16 ++++-- src/codegen/shared/configs/session_configs.py | 54 +++++++++---------- 4 files changed, 54 insertions(+), 38 deletions(-) rename src/codegen/shared/configs/models/{global_session.py => global_config.py} (65%) diff --git a/src/codegen/cli/commands/config/main.py b/src/codegen/cli/commands/config/main.py index fdb13a0a5..86a50dd4d 100644 --- a/src/codegen/cli/commands/config/main.py +++ b/src/codegen/cli/commands/config/main.py @@ -7,6 +7,7 @@ from codegen.cli.auth.session import CodegenSession from codegen.cli.workspace.decorators import requires_init +from codegen.shared.configs.session_configs import global_config @click.group(name="config") @@ -17,7 +18,8 @@ def config_command(): @config_command.command(name="list") @requires_init -def list_command(session: CodegenSession): +@click.option("--global", "is_global", is_flag=True, help="Lists the global configuration values") +def list_command(session: CodegenSession, is_global: bool): """List current configuration values.""" table = Table(title="Configuration Values", border_style="blue", show_header=True) table.add_column("Key", style="cyan", no_wrap=True) @@ -37,7 +39,8 @@ def flatten_dict(data: dict, prefix: str = "") -> dict: return items # Get flattened config and sort by keys - flat_config = flatten_dict(session.config.model_dump()) + config = global_config.global_session if is_global else session.config + flat_config = flatten_dict(config.model_dump()) sorted_items = sorted(flat_config.items(), key=lambda x: x[0]) # Group by top-level prefix @@ -58,9 +61,11 @@ def get_prefix(item): @config_command.command(name="get") @requires_init @click.argument("key") -def get_command(session: CodegenSession, key: str): +@click.option("--global", "is_global", is_flag=True, help="Get the global configuration value") +def get_command(session: CodegenSession, key: str, is_global: bool): """Get a configuration value.""" - value = session.config.get(key) + config = global_config.global_session if is_global else session.config + value = config.get(key) if value is None: rich.print(f"[red]Error: Configuration key '{key}' not found[/red]") return @@ -72,16 +77,18 @@ def get_command(session: CodegenSession, key: str): @requires_init @click.argument("key") @click.argument("value") -def set_command(session: CodegenSession, key: str, value: str): +@click.option("--global", "is_global", is_flag=True, help="Sets the global configuration value") +def set_command(session: CodegenSession, key: str, value: str, is_global: bool): """Set a configuration value and write to config.toml.""" - cur_value = session.config.get(key) + config = global_config.global_session if is_global else session.config + cur_value = config.get(key) if cur_value is None: rich.print(f"[red]Error: Configuration key '{key}' not found[/red]") return if cur_value.lower() != value.lower(): try: - session.config.set(key, value) + config.set(key, value) except Exception as e: logging.exception(e) rich.print(f"[red]{e}[/red]") diff --git a/src/codegen/shared/configs/constants.py b/src/codegen/shared/configs/constants.py index dc58327d0..a06faa06f 100644 --- a/src/codegen/shared/configs/constants.py +++ b/src/codegen/shared/configs/constants.py @@ -18,3 +18,4 @@ GLOBAL_CONFIG_DIR = Path("~/.config/codegen-sh").expanduser() AUTH_FILE = GLOBAL_CONFIG_DIR / "auth.json" SESSION_FILE = GLOBAL_CONFIG_DIR / "session.json" +GLOBAL_CONFIG_PATH = GLOBAL_CONFIG_DIR / CONFIG_FILENAME diff --git a/src/codegen/shared/configs/models/global_session.py b/src/codegen/shared/configs/models/global_config.py similarity index 65% rename from src/codegen/shared/configs/models/global_session.py rename to src/codegen/shared/configs/models/global_config.py index 0e68f8391..691751269 100644 --- a/src/codegen/shared/configs/models/global_session.py +++ b/src/codegen/shared/configs/models/global_config.py @@ -6,11 +6,13 @@ from pydantic_settings import BaseSettings from codegen.shared.configs.constants import SESSION_FILE +from codegen.shared.configs.models.session import SessionConfig -class GlobalSessionConfig(BaseSettings): +class GlobalConfig(BaseSettings): active_session_path: str | None = None sessions: list[str] + global_session: SessionConfig def get_session(self, session_root_path: Path) -> str | None: return next((s for s in self.sessions if s == str(session_root_path)), None) @@ -27,8 +29,8 @@ def set_active_session(self, session_root_path: Path) -> None: raise ValueError(msg) self.active_session_path = str(session_root_path) - if session_root_path.name not in self.sessions: - self.sessions.append(str(session_root_path)) + if self.active_session_path not in self.sessions: + self.sessions.append(self.active_session_path) self.save() @@ -38,3 +40,11 @@ def save(self) -> None: with open(SESSION_FILE, "w") as f: json.dump(self.model_dump(), f) + + self.global_session.save() + + def __str__(self) -> str: + active = self.active_session_path or "None" + sessions_str = "\n ".join(self.sessions) if self.sessions else "None" + + return f"GlobalConfig:\n Active Session: {active}\n Sessions:\n {sessions_str}\n Global Session:\n {self.global_session}" diff --git a/src/codegen/shared/configs/session_configs.py b/src/codegen/shared/configs/session_configs.py index e105bd6e7..3285a7563 100644 --- a/src/codegen/shared/configs/session_configs.py +++ b/src/codegen/shared/configs/session_configs.py @@ -3,22 +3,18 @@ import tomllib -from codegen.shared.configs.constants import CONFIG_PATH, SESSION_FILE -from codegen.shared.configs.models.global_session import GlobalSessionConfig +from codegen.shared.configs.constants import CONFIG_PATH, GLOBAL_CONFIG_PATH, SESSION_FILE +from codegen.shared.configs.models.global_config import GlobalConfig from codegen.shared.configs.models.session import SessionConfig -def load_session_config(config_path: Path) -> SessionConfig: +def load_session_config(config_path: Path, base_config: SessionConfig | None = None) -> SessionConfig: """Loads configuration from various sources.""" - # Load from .env file - env_config = _load_from_env(config_path) - - # Load from .codegen/config.toml file + base_config = base_config or global_config.global_session toml_config = _load_from_toml(config_path) - - # Merge configurations recursively - config_dict = _merge_configs(env_config.model_dump(), toml_config.model_dump()) + config_dict = _merge_configs(base_config.model_dump(), toml_config) loaded_config = SessionConfig(**config_dict) + loaded_config.file_path = str(config_path) # Save the configuration to file if it doesn't exist if not config_path.exists(): @@ -26,20 +22,34 @@ def load_session_config(config_path: Path) -> SessionConfig: return loaded_config +def _load_global_config() -> GlobalConfig: + """Load configuration from the JSON file.""" + base_session = _load_from_env(GLOBAL_CONFIG_PATH) + global_session = load_session_config(GLOBAL_CONFIG_PATH, base_config=base_session) + + if SESSION_FILE.exists(): + with open(SESSION_FILE) as f: + json_config = json.load(f) + json_config["global_session"] = global_session.model_dump() + return GlobalConfig.model_validate(json_config, strict=False) + + new_config = GlobalConfig(sessions=[], global_session=global_session) + new_config.save() + return new_config + + def _load_from_env(config_path: Path) -> SessionConfig: """Load configuration from the environment variables.""" return SessionConfig(file_path=str(config_path)) -def _load_from_toml(config_path: Path) -> SessionConfig: +def _load_from_toml(config_path: Path) -> dict[str, any]: """Load configuration from the TOML file.""" if config_path.exists(): with open(config_path, "rb") as f: toml_config = tomllib.load(f) - toml_config["file_path"] = str(config_path) - return SessionConfig.model_validate(toml_config, strict=False) - - return SessionConfig(file_path=str(config_path)) + return toml_config + return {} def _merge_configs(base: dict, override: dict) -> dict: @@ -55,20 +65,8 @@ def _merge_configs(base: dict, override: dict) -> dict: return merged -def _load_global_config() -> GlobalSessionConfig: - """Load configuration from the JSON file.""" - if SESSION_FILE.exists(): - with open(SESSION_FILE) as f: - json_config = json.load(f) - return GlobalSessionConfig.model_validate(json_config, strict=False) - - new_config = GlobalSessionConfig(sessions=[]) - new_config.save() - return new_config - - -config = load_session_config(CONFIG_PATH) global_config = _load_global_config() +config = load_session_config(CONFIG_PATH) if __name__ == "__main__": From f1342bfddd3d4a66a24753004ec33ab628acfab0 Mon Sep 17 00:00:00 2001 From: Carol Jung Date: Thu, 13 Feb 2025 14:07:16 -0800 Subject: [PATCH 2/2] fix tests --- tests/shared/configs/sample_config.py | 2 +- .../codegen/shared/configs/test_config.py | 31 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/shared/configs/sample_config.py b/tests/shared/configs/sample_config.py index b3a5b0ced..a02aa2cc2 100644 --- a/tests/shared/configs/sample_config.py +++ b/tests/shared/configs/sample_config.py @@ -5,7 +5,7 @@ openai_api_key = "sk-123456" [repository] -organization_name = "test-org" +full_name = "test-org/test-repo" repo_name = "test-repo" [feature_flags.codebase] diff --git a/tests/unit/codegen/shared/configs/test_config.py b/tests/unit/codegen/shared/configs/test_config.py index 4d9bc8890..8ef098b65 100644 --- a/tests/unit/codegen/shared/configs/test_config.py +++ b/tests/unit/codegen/shared/configs/test_config.py @@ -43,22 +43,22 @@ def test_merge_configs_empty_string(): # Test _load_from_toml def test_load_from_toml_existing_file(temp_config_file): config = _load_from_toml(temp_config_file) - assert isinstance(config, SessionConfig) - assert config.secrets.github_token == "gh_token123" - assert config.repository.repo_name == "test-repo" - assert config.feature_flags.codebase.debug is True - assert config.feature_flags.codebase.typescript.ts_dependency_manager is True - assert config.feature_flags.codebase.import_resolution_overrides == {"@org/pkg": "./local/path"} + assert isinstance(config, dict) + assert config["secrets"]["github_token"] == "gh_token123" + assert config["repository"]["full_name"] == "test-org/test-repo" + assert config["feature_flags"]["codebase"]["debug"] is True + assert config["feature_flags"]["codebase"]["typescript"]["ts_dependency_manager"] is True + assert config["feature_flags"]["codebase"]["import_resolution_overrides"] == {"@org/pkg": "./local/path"} @patch.dict("os.environ", {}) @patch("codegen.shared.configs.models.secrets.SecretsConfig.model_config", {"env_file": "nonexistent.env"}) def test_load_from_toml_nonexistent_file(): config = _load_from_toml(Path("nonexistent.toml")) - assert isinstance(config, SessionConfig) - assert config.secrets.github_token is None - assert config.repository.full_name is None - assert config.feature_flags.codebase.debug is False + assert isinstance(config, dict) + assert "secrets" not in config + assert "repository" not in config + assert "feature_flags" not in config # Test _load_from_env @@ -72,21 +72,20 @@ def test_load_from_env(): # Test load function @patch.dict("os.environ", {}, clear=True) # Clear all env vars for this test -@patch("codegen.shared.configs.session_configs._load_from_env") @patch("codegen.shared.configs.session_configs._load_from_toml") @patch("codegen.shared.configs.models.secrets.SecretsConfig.model_config", {"env_file": None, "env_prefix": "CODEGEN_SECRETS__"}) -def test_load_with_both_configs(mock_toml, mock_env): +def test_load_with_both_configs(mock_toml): # Setup mock returns - mock_env.return_value = SessionConfig(file_path=str(CONFIG_PATH), secrets=SecretsConfig(github_token="env_token"), feature_flags=FeatureFlagsConfig(codebase=CodebaseFeatureFlags(debug=True))) - mock_toml.return_value = SessionConfig(file_path=str(CONFIG_PATH), secrets={"openai_api_key": "openai_key"}, repository={"full_name": "codegen-org/test-repo"}) + base_config = SessionConfig(file_path=str(CONFIG_PATH), secrets=SecretsConfig(github_token="env_token"), feature_flags=FeatureFlagsConfig(codebase=CodebaseFeatureFlags(debug=True))) + mock_toml.return_value = {"secrets": {"openai_api_key": "openai_key"}, "repository": {"full_name": "codegen-org/test-repo"}} - config = load_session_config(CONFIG_PATH) + config = load_session_config(CONFIG_PATH, base_config) assert isinstance(config, SessionConfig) assert config.secrets.github_token == "env_token" assert config.secrets.openai_api_key == "openai_key" assert config.repository.full_name == "codegen-org/test-repo" - assert config.feature_flags.codebase.debug is False + assert config.feature_flags.codebase.debug is True # Error cases