11"""Main CLI tool for document operations."""
22
3+ from collections .abc import Sequence
34import logging
45from pathlib import Path
56import sys
67from typing import Any , cast
78
89import click
9- from pydantic import ValidationError
10+ from pydantic import BaseModel , ValidationError
1011import rich .console
1112from rich .traceback import install as install_traceback
1213
@@ -47,6 +48,33 @@ def _setup_console() -> None:
4748 install_traceback (console = CONSOLE , show_locals = True )
4849
4950
51+ def _handle_validation_error (
52+ e : Exception ,
53+ model_class : type [BaseModel ],
54+ config_files : Sequence [Path ] | None ,
55+ verbose : int ,
56+ ctx : click .Context ,
57+ ) -> None :
58+ """Format validation errors and exit the CLI.
59+
60+ Outsourced logic to avoid code duplication between App and Env config phases.
61+ Using Sequence[Path] ensures compatibility with both lists and tuples.
62+ """
63+ config_label = "Application" if model_class == AppConfig else "Environment"
64+
65+ if isinstance (e , ValidationError ):
66+ # Safely extract the first config file name for the error header
67+ config_file = str ((config_files or ["unknown" ])[0 ])
68+ format_pydantic_error (
69+ e , model_class , config_file , verbose , console = CONSOLE
70+ )
71+ else :
72+ log .error ("%s configuration failed validation:" , config_label )
73+ log .error ("Error in config file(s): %s" , config_files )
74+ log .error (e )
75+ ctx .exit (1 )
76+
77+
5078@click .group (
5179 name = APP_NAME ,
5280 context_settings = {"show_default" : True , "help_option_names" : ["-h" , "--help" ]},
@@ -103,37 +131,24 @@ def cli(
103131 env_config : Path ,
104132 ** kwargs : dict ,
105133) -> None :
106- """Acts as a main entry point for CLI tool.
107-
108- :param ctx: The Click context object.
109- :param verbose: The verbosity level.
110- :param dry_run: If set, just pretend to run the command without making any changes.
111- :param debug: If set, enable debug mode.
112- :param app_config: Filename to the application TOML config file.
113- :param env_config: Filename to a environment's TOML config file.
114- :param kwargs: Additional keyword arguments.
115- """
134+ """Acts as a main entry point for CLI tool."""
116135 if ctx .invoked_subcommand is None :
117- # If no subcommand is invoked, show the help message
118136 click .echo (10 * "-" )
119137 click .echo (ctx .get_help ())
120138 ctx .exit (0 )
121139
122140 if ctx .obj is None :
123141 ctx .ensure_object (DocBuildContext )
124142
125- # Build the context object
126143 context = ctx .obj
127144 context .verbose = verbose
128145 context .dry_run = dry_run
129146 context .debug = debug
130147
131- # --- PHASE 1: Load and Validate Application Config (and setup logging) ---
132- #
133- # 1. Load the raw application config dictionary
148+ # --- PHASE 1: Load and Validate Application Config ---
134149 (
135150 context .appconfigfiles ,
136- raw_appconfig , # Store config as raw dictionary
151+ raw_appconfig ,
137152 context .appconfig_from_defaults ,
138153 ) = handle_config (
139154 app_config ,
@@ -143,39 +158,23 @@ def cli(
143158 DEFAULT_APP_CONFIG ,
144159 )
145160
146- # Explicitly cast the raw_appconfig type to silence Pylance
147161 raw_appconfig = cast (dict [str , Any ], raw_appconfig )
148162
149- # 2. Validate the raw config dictionary using Pydantic
150163 try :
151- # Pydantic validation also handles placeholder replacement via @model_validator
152164 context .appconfig = AppConfig .from_dict (raw_appconfig )
153165 except (ValueError , ValidationError ) as e :
154- if isinstance (e , ValidationError ):
155- # FIXED: Added fallback for config files to avoid subscripting None
156- config_file = str ((context .appconfigfiles or ["unknown" ])[0 ])
157- format_pydantic_error (
158- e , AppConfig , config_file , context .verbose
159- )
160- else :
161- log .error ("Application configuration failed validation:" )
162- log .error ("Error in config file(s): %s" , context .appconfigfiles )
163- log .error (e )
164- ctx .exit (1 )
166+ _handle_validation_error (e , AppConfig , context .appconfigfiles , verbose , ctx )
165167
166168 # 3. Setup logging using the validated config object
167- # Use model_dump(by_alias=True) to ensure the 'class' alias is used.
168169 logging_config = context .appconfig .logging .model_dump (
169170 by_alias = True , exclude_none = True
170171 )
171172 setup_logging (cliverbosity = verbose , user_config = {"logging" : logging_config })
172173
173174 # --- PHASE 2: Load Environment Config, Validate, and Acquire Lock ---
174- #
175- # 1. Load raw Environment Config
176175 (
177176 context .envconfigfiles ,
178- raw_envconfig , # Renaming context.envconfig to raw_envconfig locally
177+ raw_envconfig ,
179178 context .envconfig_from_defaults ,
180179 ) = handle_config (
181180 env_config ,
@@ -185,57 +184,32 @@ def cli(
185184 DEFAULT_ENV_CONFIG ,
186185 )
187186
188- # Explicitly cast the raw_envconfig type to silence Pylance
189187 raw_envconfig = cast (dict [str , Any ], raw_envconfig )
190188
191- # 2. VALIDATE the raw environment config dictionary using Pydantic
192189 try :
193- # Pydantic validation handles placeholder replacement via @model_validator
194- # The result is the validated Pydantic object, stored in context.envconfig
195190 context .envconfig = EnvConfig .from_dict (raw_envconfig )
196191 except (ValueError , ValidationError ) as e :
197- if isinstance (e , ValidationError ):
198- # FIXED: Added fallback for config files to avoid subscripting None
199- config_file = str ((context .envconfigfiles or ["unknown" ])[0 ])
200- format_pydantic_error (
201- e , EnvConfig , config_file , context .verbose
202- )
203- else :
204- log .error (
205- "Environment configuration failed validation: "
206- "Error in config file(s): %s %s" ,
207- context .envconfigfiles ,
208- e ,
209- )
210- ctx .exit (1 )
192+ _handle_validation_error (e , EnvConfig , context .envconfigfiles , verbose , ctx )
211193
212194 env_config_path = (context .envconfigfiles or [None ])[0 ]
213195
214- # --- CONCURRENCY CONTROL: Use explicit __enter__ and cleanup registration ---
196+ # --- CONCURRENCY CONTROL ---
215197 if env_config_path :
216- # 1. Instantiate the lock object
217198 ctx .obj .env_lock = PidFileLock (resource_path = cast (Path , env_config_path ))
218-
219199 try :
220- # 2. Acquire the lock by explicitly calling the __enter__ method.
221200 ctx .obj .env_lock .__enter__ ()
222201 log .info ("Acquired lock for environment config: %r" , env_config_path .name )
223-
224202 except LockAcquisitionError as e :
225- # Handles lock contention
226203 log .error (str (e ))
227204 ctx .exit (1 )
228205 except Exception as e :
229- # Handles critical errors
230206 log .error ("Failed to set up environment lock: %s" , e )
231207 ctx .exit (1 )
232208
233- # 3. Register the lock's __exit__ method to be called when the context
234- # terminates.
235209 ctx .call_on_close (lambda : ctx .obj .env_lock .__exit__ (None , None , None ))
236210
237211
238- # Add subcommand
212+ # Add subcommands
239213cli .add_command (build )
240214cli .add_command (c14n )
241215cli .add_command (config )
0 commit comments