"""Configuration management commands."""
import os
import subprocess
import sys
import typer
from dbx_python_cli.utils.output import paginate_output, should_use_pager
from dbx_python_cli.utils.repo import (
get_config_path,
get_default_config_path,
get_config,
)
app = typer.Typer(
help="Configuration management commands",
context_settings={"help_option_names": ["-h", "--help"]},
no_args_is_help=True,
)
[docs]
@app.command()
def init(
yes: bool = typer.Option(
False,
"--yes",
"-y",
help="Skip confirmation prompt and overwrite existing config",
),
):
"""Initialize user configuration file."""
user_config_path = get_config_path()
default_config_path = get_default_config_path()
if user_config_path.exists():
typer.echo(f"Configuration file already exists at {user_config_path}")
if not yes:
overwrite = typer.confirm("Do you want to overwrite it?")
if not overwrite:
typer.echo("Aborted.")
raise typer.Exit(0)
# Create config directory if it doesn't exist
user_config_path.parent.mkdir(parents=True, exist_ok=True)
# Copy default config to user config
if default_config_path.exists():
import shutil
shutil.copy(default_config_path, user_config_path)
typer.echo(f"โ
Configuration file created at {user_config_path}")
typer.echo("\nYou can now edit this file to customize your repository groups.")
else:
typer.echo(
f"Error: Default configuration not found at {default_config_path}",
err=True,
)
raise typer.Exit(1)
[docs]
@app.command()
def edit():
"""Edit the configuration file with your default editor.
Opens the configuration file using the editor specified in the EDITOR
environment variable. If EDITOR is not set, falls back to common editors
(vim, nano, vi) or uses 'open' on macOS.
Examples::
dbx config edit # Open with default editor
EDITOR=code dbx config edit # Open with VS Code
EDITOR=nano dbx config edit # Open with nano
"""
config_path = get_config_path()
if not config_path.exists():
typer.echo(f"โ Configuration file not found at {config_path}", err=True)
typer.echo("\nCreate it first using: dbx config init")
raise typer.Exit(1)
# Get editor from environment variable
editor = os.environ.get("EDITOR")
if not editor:
# Try common editors in order of preference
common_editors = ["vim", "nano", "vi"]
for candidate in common_editors:
try:
# Check if editor exists in PATH
subprocess.run(
["which", candidate],
check=True,
capture_output=True,
)
editor = candidate
break
except subprocess.CalledProcessError:
continue
# If no common editor found, try 'open' on macOS
if not editor:
import platform
if platform.system() == "Darwin":
editor = "open"
else:
typer.echo(
"โ No editor found. Please set the EDITOR environment variable.",
err=True,
)
typer.echo("\nExample: export EDITOR=nano")
raise typer.Exit(1)
typer.echo(f"๐ Opening {config_path} with {editor}...")
try:
# Open the editor
result = subprocess.run([editor, str(config_path)])
if result.returncode == 0:
typer.echo("โ
Configuration file saved")
else:
typer.echo(
f"โ ๏ธ Editor exited with code {result.returncode}",
err=True,
)
raise typer.Exit(result.returncode)
except FileNotFoundError:
typer.echo(
f"โ Editor '{editor}' not found. Please check your EDITOR environment variable.",
err=True,
)
raise typer.Exit(1)
except KeyboardInterrupt:
typer.echo("\nโ ๏ธ Editing cancelled")
raise typer.Exit(130)
[docs]
@app.command()
def show(ctx: typer.Context):
"""Display the current configuration.
Shows the active configuration being used by dbx, including the config file
location and all settings.
Examples::
dbx config show # Display current configuration
"""
config_path = get_config_path()
default_config_path = get_default_config_path()
# Determine which config is being used
if config_path.exists():
active_config_path = config_path
config_source = "user config"
elif default_config_path.exists():
active_config_path = default_config_path
config_source = "default config"
else:
typer.echo("โ No configuration file found", err=True)
typer.echo("\nCreate one using: dbx config init")
raise typer.Exit(1)
# Helpers
def h(text):
"""Bold cyan section header."""
return typer.style(text, fg=typer.colors.CYAN, bold=True)
def key(text):
"""Bold label."""
return typer.style(text, bold=True)
def val(text):
"""Green value."""
return typer.style(str(text), fg=typer.colors.GREEN)
def dim(text):
"""Dimmed hint text."""
return typer.style(str(text), fg=typer.colors.BRIGHT_BLACK)
def sub(text):
"""Yellow sub-section label."""
return typer.style(text, fg=typer.colors.YELLOW)
# Collect all output into a buffer so we can paginate it
buf = []
buf.append(typer.style(f"๐ Configuration ({config_source})", bold=True))
buf.append(f"{key('Location:')} {active_config_path}")
# Show Python version
python_version = (
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
)
buf.append(f"{key('Python:')} {val(python_version)}\n")
# Load and display the config
try:
config = get_config()
# Project default environment variables
project_config = config.get("project", {})
default_env = project_config.get("default_env", {})
if default_env:
buf.append(h("Default Environment Variables"))
for var_name, var_value in sorted(default_env.items()):
buf.append(
f" {typer.style(var_name, fg=typer.colors.MAGENTA)}={val(var_value)}"
)
buf.append("")
# Repository settings
repo_config = config.get("repo", {})
if repo_config:
buf.append(h("Repository Settings"))
buf.append(
f" {key('base_dir:')} {val(repo_config.get('base_dir', 'Not set'))}"
)
fork_user = repo_config.get("fork_user")
buf.append(
f" {key('fork_user:')} {val(fork_user) if fork_user else dim('Not set')}"
)
buf.append("")
# Repository groups
groups = repo_config.get("groups", {})
if groups:
buf.append(h(f"Repository Groups ({len(groups)})"))
for group_name, group_config in sorted(groups.items()):
repos = group_config.get("repos", [])
n = len(repos)
buf.append(
f"\n {typer.style('โ', fg=typer.colors.CYAN)} "
f"{typer.style(group_name, bold=True)}"
f" {dim(f'({n} repo' + ('s' if n != 1 else '') + ')')}"
)
for repo_url in repos:
repo_name = repo_url.split("/")[-1].replace(".git", "")
buf.append(f" {dim('โ')} {repo_name}")
# Python version for the group
python_version = group_config.get("python_version")
if python_version:
buf.append(f"\n {sub('Python version:')} {python_version}")
# Install directories
install_dirs = group_config.get("install_dirs", {})
if install_dirs:
buf.append(f"\n {sub('Install dirs:')}")
for rname, dirs in install_dirs.items():
buf.append(f" {dim(rname + ':')}")
for dir_path in dirs:
buf.append(f" {dim('ยท')} {dir_path}")
# Preferred branch
preferred_branch = group_config.get("preferred_branch", {})
if preferred_branch:
buf.append(f"\n {sub('Preferred branch:')}")
for rname, branch in preferred_branch.items():
buf.append(f" {dim(rname + ':')} {branch}")
# Custom test runners
test_runner = group_config.get("test_runner", {})
if test_runner:
buf.append(f"\n {sub('Test runner:')}")
for rname, runner_path in test_runner.items():
buf.append(f" {dim(rname + ':')} {runner_path}")
# Test environment variables
test_env = group_config.get("test_env", {})
if test_env:
buf.append(f"\n {sub('Test env:')}")
for rname, env_vars in test_env.items():
if isinstance(env_vars, dict):
buf.append(f" {dim(rname + ':')}")
for var_name, var_value in env_vars.items():
buf.append(
f" {typer.style(var_name, fg=typer.colors.MAGENTA)}={var_value}"
)
buf.append("")
else:
buf.append(dim("No repository groups configured\n"))
buf.append(dim(" dbx config edit โ open in editor"))
buf.append(dim(" dbx config init โ (re)create from default"))
except Exception as e:
typer.echo(f"โ Error reading configuration: {e}", err=True)
raise typer.Exit(1)
output = "\n".join(buf)
# Use pager if requested
use_pager = should_use_pager(ctx, command_default=False)
paginate_output(output, use_pager)
[docs]
@app.command()
def validate(ctx: typer.Context):
"""Validate the configuration file for unknown or deprecated keys."""
KNOWN_TOP_LEVEL = {"project", "repo", "evergreen"}
KNOWN_REPO_KEYS = {
"base_dir",
"fork_user",
"python_version",
"global_groups",
"group_priority",
"flat",
"editor",
"groups",
}
KNOWN_GROUP_KEYS = {
"repos",
"python_version",
"preferred_branch",
"no_fork",
"upstream",
"upstream_branch",
"sync_after_clone",
"install_extras",
"install_groups",
"install_dirs",
"build_commands",
"skip_install",
"test_runner",
"test_runner_args",
"test_env",
"sys_path",
}
KNOWN_PROJECT_KEYS = {"default_env", "mongodb"}
issues: list[str] = []
has_errors = False
try:
config = get_config()
except Exception as e:
typer.echo(f"โ Failed to load configuration: {e}", err=True)
raise typer.Exit(1)
# Check for unknown top-level sections
for key in config:
if key not in KNOWN_TOP_LEVEL:
issues.append(f"โ ๏ธ [unknown] top-level key: [{key}]")
has_errors = True
# Check [repo] keys
repo_config = config.get("repo", {})
if isinstance(repo_config, dict):
# base_dir is required
if "base_dir" not in repo_config:
issues.append("โ [error] [repo] is missing required key: base_dir")
has_errors = True
for key in repo_config:
if key == "groups":
continue
if key not in KNOWN_REPO_KEYS:
issues.append(f"โ ๏ธ [unknown] [repo] key: {key}")
has_errors = True
# Check [repo.groups.X] keys
groups = repo_config.get("groups", {})
if isinstance(groups, dict):
for group_name, group_cfg in groups.items():
if not isinstance(group_cfg, dict):
continue
for key in group_cfg:
if key not in KNOWN_GROUP_KEYS:
issues.append(
f"โ ๏ธ [unknown] [repo.groups.{group_name}] key: {key}"
)
has_errors = True
# Check [project] keys
project_config = config.get("project", {})
if isinstance(project_config, dict):
for key in project_config:
if key not in KNOWN_PROJECT_KEYS:
issues.append(f"โ ๏ธ [unknown] [project] key: {key}")
has_errors = True
# Check [evergreen.X] keys โ each sub-table may only have project_name
evergreen_config = config.get("evergreen", {})
if isinstance(evergreen_config, dict):
for section_name, section_cfg in evergreen_config.items():
if not isinstance(section_cfg, dict):
continue
for key in section_cfg:
if key != "project_name":
issues.append(f"โ ๏ธ [unknown] [evergreen.{section_name}] key: {key}")
has_errors = True
# Report results
if issues:
for issue in issues:
typer.echo(issue)
typer.echo(f"\n{len(issues)} issue(s) found.")
raise typer.Exit(1 if has_errors else 0)
else:
typer.echo(
"โ
Configuration looks valid โ no unknown or deprecated keys found."
)