Skip to content

CG-10801: support --global flag in config cmds #487

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/codegen/cli/commands/config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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]")
Expand Down
1 change: 1 addition & 0 deletions src/codegen/shared/configs/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()

Expand All @@ -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}"
54 changes: 26 additions & 28 deletions src/codegen/shared/configs/session_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,53 @@

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():
loaded_config.save()
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]:

Check failure on line 46 in src/codegen/shared/configs/session_configs.py

View workflow job for this annotation

GitHub Actions / mypy

error: Function "builtins.any" is not valid as a type [valid-type]
"""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:
Expand All @@ -55,20 +65,8 @@
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__":
Expand Down
2 changes: 1 addition & 1 deletion tests/shared/configs/sample_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
31 changes: 15 additions & 16 deletions tests/unit/codegen/shared/configs/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading