|
4 | 4 | import logging |
5 | 5 | from pathlib import Path |
6 | 6 | import sys |
| 7 | +import tomllib |
7 | 8 | from typing import Any, cast |
8 | 9 |
|
9 | 10 | import click |
|
24 | 25 | from ..logging import setup_logging |
25 | 26 | from ..models.config.app import AppConfig |
26 | 27 | from ..models.config.env import EnvConfig |
27 | | -from ..utils.errors import format_pydantic_error |
| 28 | +from ..utils.errors import format_pydantic_error, format_toml_error |
28 | 29 | from ..utils.pidlock import LockAcquisitionError, PidFileLock |
29 | 30 | from .cmd_build import build |
30 | 31 | from .cmd_c14n import c14n |
@@ -68,21 +69,70 @@ def handle_validation_error( |
68 | 69 | :param ctx: The Click context, used to exit the CLI with an appropriate |
69 | 70 | status code after handling the error. |
70 | 71 | """ |
71 | | - config_label = "Application" if model_class == AppConfig else "Environment" |
| 72 | + # Determine which file we were working on |
| 73 | + config_file = str((config_files or ["unknown"])[0]) |
72 | 74 |
|
73 | | - if isinstance(e, ValidationError): |
74 | | - # Safely extract the first config file name for the error header |
75 | | - config_file = str((config_files or ["unknown"])[0]) |
76 | | - format_pydantic_error( |
77 | | - e, model_class, config_file, verbose, console=CONSOLE |
78 | | - ) |
| 75 | + if isinstance(e, tomllib.TOMLDecodeError): |
| 76 | + format_toml_error(e, config_file, console=CONSOLE) |
| 77 | + elif isinstance(e, ValidationError): |
| 78 | + format_pydantic_error(e, model_class, config_file, verbose, console=CONSOLE) |
79 | 79 | else: |
80 | | - log.error("%s configuration failed validation:", config_label) |
| 80 | + config_label = "Application" if model_class == AppConfig else "Environment" |
| 81 | + log.error("%s configuration failed:", config_label) |
81 | 82 | log.error("Error in config file(s): %s", config_files) |
82 | 83 | log.error(e) |
83 | 84 | ctx.exit(1) |
84 | 85 |
|
85 | 86 |
|
| 87 | +def load_app_config( |
| 88 | + ctx: click.Context, |
| 89 | + app_config: Path, |
| 90 | + max_workers: str | None |
| 91 | +) -> None: |
| 92 | + """Load and validate Application configuration. |
| 93 | +
|
| 94 | + :param ctx: The Click context object. The result will be added to ``ctx.obj.appconfig``. |
| 95 | + :param app_config: The path to the application config file provided via CLI. |
| 96 | + :param max_workers: The max_workers value from CLI options. |
| 97 | + """ |
| 98 | + context = ctx.obj |
| 99 | + result = handle_config( |
| 100 | + app_config, |
| 101 | + CONFIG_PATHS, |
| 102 | + APP_CONFIG_BASENAMES + PROJECT_LEVEL_APP_CONFIG_FILENAMES, |
| 103 | + None, |
| 104 | + DEFAULT_APP_CONFIG, |
| 105 | + ) |
| 106 | + context.appconfigfiles, raw_appconfig, context.appconfig_from_defaults = cast( |
| 107 | + tuple[tuple[Path, ...] | None, dict[str, Any], bool], result |
| 108 | + ) |
| 109 | + |
| 110 | + if max_workers is not None: |
| 111 | + raw_appconfig["max_workers"] = max_workers |
| 112 | + |
| 113 | + context.appconfig = AppConfig.from_dict(raw_appconfig) |
| 114 | + |
| 115 | + |
| 116 | +def load_env_config(ctx: click.Context, env_config: Path) -> None: |
| 117 | + """Load and validate Environment configuration. |
| 118 | +
|
| 119 | + :param ctx: The Click context object. The result will be added to ``ctx.obj.envconfig``. |
| 120 | + :param env_config: The path to the environment config file provided via CLI. |
| 121 | + """ |
| 122 | + context = ctx.obj |
| 123 | + result = handle_config( |
| 124 | + env_config, |
| 125 | + (PROJECT_DIR,), |
| 126 | + None, |
| 127 | + DEFAULT_ENV_CONFIG_FILENAME, |
| 128 | + DEFAULT_ENV_CONFIG, |
| 129 | + ) |
| 130 | + context.envconfigfiles, raw_envconfig, context.envconfig_from_defaults = cast( |
| 131 | + tuple[tuple[Path, ...] | None, dict[str, Any], bool], result |
| 132 | + ) |
| 133 | + |
| 134 | + context.envconfig = EnvConfig.from_dict(raw_envconfig) |
| 135 | + |
86 | 136 | @click.group( |
87 | 137 | name=APP_NAME, |
88 | 138 | context_settings={"show_default": True, "help_option_names": ["-h", "--help"]}, |
@@ -167,76 +217,48 @@ def cli( |
167 | 217 | ctx.ensure_object(DocBuildContext) |
168 | 218 |
|
169 | 219 | context = ctx.obj |
170 | | - context.verbose = verbose |
171 | | - context.dry_run = dry_run |
172 | | - context.debug = debug |
173 | | - |
174 | | - # --- PHASE 1: Load and Validate Application Config --- |
175 | | - ( |
176 | | - context.appconfigfiles, |
177 | | - raw_appconfig, |
178 | | - context.appconfig_from_defaults, |
179 | | - ) = handle_config( |
180 | | - app_config, |
181 | | - CONFIG_PATHS, |
182 | | - APP_CONFIG_BASENAMES + PROJECT_LEVEL_APP_CONFIG_FILENAMES, |
183 | | - None, |
184 | | - DEFAULT_APP_CONFIG, |
185 | | - ) |
186 | | - |
187 | | - raw_appconfig = cast(dict[str, Any], raw_appconfig) |
| 220 | + context.verbose, context.dry_run, context.debug = verbose, dry_run, debug |
188 | 221 |
|
189 | | - if max_workers is not None: |
190 | | - raw_appconfig["max_workers"] = max_workers |
| 222 | + # State tracking for centralized error handling |
| 223 | + current_model: type[BaseModel] = AppConfig |
| 224 | + current_files: Sequence[Path] | None = None |
191 | 225 |
|
192 | 226 | try: |
193 | | - context.appconfig = AppConfig.from_dict(raw_appconfig) |
194 | | - except (ValueError, ValidationError) as e: |
195 | | - handle_validation_error(e, AppConfig, context.appconfigfiles, verbose, ctx) |
| 227 | + # --- PHASE 1: Load Application Config --- |
| 228 | + current_model = AppConfig |
| 229 | + current_files = (app_config,) if app_config else None |
| 230 | + load_app_config(ctx, app_config, max_workers) |
196 | 231 |
|
197 | | - # 3. Setup logging using the validated config object |
198 | | - logging_config = context.appconfig.logging.model_dump( |
199 | | - by_alias=True, exclude_none=True |
200 | | - ) |
201 | | - setup_logging(cliverbosity=verbose, user_config={"logging": logging_config}) |
202 | | - |
203 | | - # --- PHASE 2: Load Environment Config, Validate, and Acquire Lock --- |
204 | | - ( |
205 | | - context.envconfigfiles, |
206 | | - raw_envconfig, |
207 | | - context.envconfig_from_defaults, |
208 | | - ) = handle_config( |
209 | | - env_config, |
210 | | - (PROJECT_DIR,), |
211 | | - None, |
212 | | - DEFAULT_ENV_CONFIG_FILENAME, |
213 | | - DEFAULT_ENV_CONFIG, |
214 | | - ) |
| 232 | + # Setup logging |
| 233 | + logging_config = context.appconfig.logging.model_dump( |
| 234 | + by_alias=True, exclude_none=True |
| 235 | + ) |
| 236 | + setup_logging(cliverbosity=verbose, user_config={"logging": logging_config}) |
215 | 237 |
|
216 | | - raw_envconfig = cast(dict[str, Any], raw_envconfig) |
| 238 | + # --- PHASE 2: Load Environment Config --- |
| 239 | + current_model = EnvConfig |
| 240 | + current_files = (env_config,) if env_config else None |
| 241 | + load_env_config(ctx, env_config) |
217 | 242 |
|
218 | | - try: |
219 | | - context.envconfig = EnvConfig.from_dict(raw_envconfig) |
220 | | - except (ValueError, ValidationError) as e: |
221 | | - handle_validation_error(e, EnvConfig, context.envconfigfiles, verbose, ctx) |
| 243 | + except (ValueError, ValidationError, tomllib.TOMLDecodeError) as e: |
| 244 | + handle_validation_error(e, current_model, current_files, verbose, ctx) |
222 | 245 |
|
| 246 | + # --- PHASE 3: Setup Concurrency Lock --- |
| 247 | + # (Remains outside the try block as it has its own specialized error handling) |
223 | 248 | env_config_path = (context.envconfigfiles or [None])[0] |
224 | | - |
225 | | - # --- CONCURRENCY CONTROL --- |
226 | 249 | if env_config_path: |
227 | 250 | ctx.obj.env_lock = PidFileLock(resource_path=cast(Path, env_config_path)) |
228 | 251 | try: |
229 | 252 | ctx.obj.env_lock.__enter__() |
230 | 253 | log.info("Acquired lock for environment config: %r", env_config_path.name) |
| 254 | + ctx.call_on_close(lambda: ctx.obj.env_lock.__exit__(None, None, None)) |
231 | 255 | except LockAcquisitionError as e: |
232 | 256 | log.error(str(e)) |
233 | 257 | ctx.exit(1) |
234 | 258 | except Exception as e: |
235 | 259 | log.error("Failed to set up environment lock: %s", e) |
236 | 260 | ctx.exit(1) |
237 | 261 |
|
238 | | - ctx.call_on_close(lambda: ctx.obj.env_lock.__exit__(None, None, None)) |
239 | | - |
240 | 262 |
|
241 | 263 | # Add subcommands |
242 | 264 | cli.add_command(build) |
|
0 commit comments