1010import subprocess
1111
1212
13+ class GenerateConfigError (ValueError ):
14+ """Expected generate config load/shape error shown without traceback."""
15+
16+
1317# Generate command class
1418class GenerateCommand (Command ):
1519 def __init__ (self , parser ):
@@ -111,8 +115,7 @@ def _load_yaml_config(self, structure_definition, structures_path):
111115 structure_definition = f"file://{ structure_definition } "
112116
113117 if structure_definition .startswith ("file://" ) and structure_definition .endswith (".yaml" ):
114- with open (structure_definition [7 :], 'r' ) as f :
115- return yaml .safe_load (f )
118+ file_path = structure_definition [7 :]
116119 else :
117120 this_file = os .path .dirname (os .path .realpath (__file__ ))
118121 contribs_path = os .path .join (this_file , ".." , "contribs" )
@@ -121,11 +124,23 @@ def _load_yaml_config(self, structure_definition, structures_path):
121124 file_path = os .path .join (structures_path , f"{ structure_definition } .yaml" )
122125 if not os .path .exists (file_path ):
123126 file_path = os .path .join (contribs_path , f"{ structure_definition } .yaml" )
124- if not os .path .exists (file_path ):
125- self .logger .error (f"❗ File not found: { file_path } " )
126- return None
127+
128+ try :
127129 with open (file_path , 'r' ) as f :
128130 return yaml .safe_load (f )
131+ except FileNotFoundError :
132+ raise GenerateConfigError (f"File not found: { file_path } " ) from None
133+ except yaml .YAMLError as exc :
134+ raise GenerateConfigError (f"Invalid YAML in { file_path } : { exc } " ) from None
135+ except OSError as exc :
136+ raise GenerateConfigError (f"Failed to read { file_path } : { exc } " ) from None
137+
138+ def _validate_loaded_config (self , config ):
139+ if config is None :
140+ return {}
141+ if not isinstance (config , dict ):
142+ raise GenerateConfigError ("Top-level YAML content must be a mapping." )
143+ return config
129144
130145 def execute (self , args ):
131146 try :
@@ -164,19 +179,23 @@ def execute(self, args):
164179 self .logger .error (f"Mappings file not found: { mappings_file_path } " )
165180 return
166181
182+ # Load and validate config before creating output/backup paths, so config
183+ # errors do not leave partial filesystem side effects behind.
184+ try :
185+ config = self ._validate_loaded_config (
186+ self ._load_yaml_config (args .structure_definition , args .structures_path )
187+ )
188+ except GenerateConfigError as exc :
189+ self .logger .error (f"❗ { exc } " )
190+ raise SystemExit (1 ) from None
191+
167192 if args .backup and not os .path .exists (args .backup ):
168193 os .makedirs (args .backup )
169194
170195 if args .base_path and not os .path .exists (args .base_path ) and "console" not in args .output :
171196 self .logger .info (f"Creating base path: { args .base_path } " )
172197 os .makedirs (args .base_path )
173198
174- # Load config to check for hooks
175- config = None
176- config = self ._load_yaml_config (args .structure_definition , args .structures_path )
177- if config is None :
178- return
179-
180199 pre_hooks = config .get ('pre_hooks' , [])
181200 post_hooks = config .get ('post_hooks' , [])
182201
@@ -188,7 +207,7 @@ def execute(self, args):
188207 # Actually generate structure
189208 try :
190209 self ._create_structure (args , mappings )
191- except TemplateVariableError as exc :
210+ except ( GenerateConfigError , TemplateVariableError ) as exc :
192211 self .logger .error (f"❗ { exc } " )
193212 raise SystemExit (1 ) from None
194213
@@ -201,9 +220,9 @@ def _create_structure(self, args, mappings=None, summary=None, print_summary=Tru
201220 if isinstance (args , dict ):
202221 args = argparse .Namespace (** args )
203222
204- config = self ._load_yaml_config ( args . structure_definition , args . structures_path )
205- if config is None :
206- return summary if summary is not None else None
223+ config = self ._validate_loaded_config (
224+ self . _load_yaml_config ( args . structure_definition , args . structures_path )
225+ )
207226
208227 # Safely parse template variables
209228 template_vars = self ._parse_template_vars (args .vars ) if getattr (args , 'vars' , None ) else {}
0 commit comments