66Supports multiple input methods and validates the generated specs.
77
88Generated by: claude-4-sonnet
9- Version: 1.0.0
9+ Version: 1.0.1
1010"""
1111
1212import argparse
@@ -197,6 +197,24 @@ def log_error(self, message: str, role_prefix: bool = True):
197197 prefix = f"[{ self .current_role } ] " if role_prefix and self .current_role else ""
198198 print (f"{ prefix } { message } " )
199199
200+ def _safe_load_yaml_file (self , file_path : Path , description : str = "" ) -> Optional [Dict [str , Any ]]:
201+ """Safely load a YAML file with proper error handling"""
202+ try :
203+ with open (file_path , "r" , encoding = "utf-8" ) as f :
204+ content = f .read ()
205+ if not content .strip ():
206+ return {}
207+ return yaml .safe_load (content )
208+ except yaml .YAMLError as e :
209+ self .log_error (f"Invalid YAML in { file_path } : { e } " )
210+ return None
211+ except (OSError , IOError ) as e :
212+ self .log_error (f"Could not read { file_path } : { e } " )
213+ return None
214+ except Exception as e :
215+ self .log_verbose (f"Could not parse { file_path } : { e } " )
216+ return None
217+
200218 def log_section (self , title : str ):
201219 """Log a section header"""
202220 if self .verbosity >= 1 :
@@ -765,15 +783,15 @@ def parse_task_file_includes(self, task_file_path: Path) -> Set[str]:
765783 print (f" No includes found in { task_file_path .name } " )
766784
767785 except UnicodeDecodeError as e :
768- print (f"Error: Could not decode task file { task_file_path } : { e } " )
786+ self . log_error (f"Could not decode task file { task_file_path } : { e } " )
769787 except (OSError , IOError ) as e :
770- print (f"Error: Could not read task file { task_file_path } : { e } " )
788+ self . log_error (f"Could not read task file { task_file_path } : { e } " )
771789 except yaml .YAMLError as e :
772- print (f"Warning: YAML parsing error in { task_file_path } : { e } " )
790+ self . log_verbose (f"YAML parsing error in { task_file_path } : { e } " )
773791 except re .error as e :
774- print (f"Error: Regex pattern error in { task_file_path } : { e } " )
792+ self . log_error (f"Regex pattern error in { task_file_path } : { e } " )
775793 except Exception as e :
776- print (f"Warning: Could not parse task file { task_file_path } : { e } " )
794+ self . log_verbose (f"Could not parse task file { task_file_path } : { e } " )
777795
778796 return includes
779797
@@ -829,49 +847,25 @@ def analyze_role_structure(self, role_path: str) -> Dict[str, Any]:
829847 # Analyze defaults/main.yml
830848 defaults_file = role_dir / "defaults" / "main.yml"
831849 if defaults_file .exists ():
832- try :
833- with open (defaults_file , "r" , encoding = "utf-8" ) as f :
834- content = f .read ().strip ()
835- if content : # Only try to parse if file has content
836- defaults = yaml .safe_load (content )
837- # Handle case where YAML file contains only comments or is empty
838- analysis ["defaults" ] = (
839- defaults if isinstance (defaults , dict ) else {}
840- )
841- else :
842- analysis ["defaults" ] = {}
843- except yaml .YAMLError as e :
844- print (f"Error: Invalid YAML in { defaults_file } : { e } " )
845- analysis ["defaults" ] = {}
846- except (OSError , IOError ) as e :
847- print (f"Error: Could not read { defaults_file } : { e } " )
848- analysis ["defaults" ] = {}
849- except Exception as e :
850- print (f"Warning: Could not parse { defaults_file } : { e } " )
850+ defaults = self ._safe_load_yaml_file (defaults_file )
851+ if defaults is not None :
852+ # Handle case where YAML file contains only comments or is empty
853+ analysis ["defaults" ] = (
854+ defaults if isinstance (defaults , dict ) else {}
855+ )
856+ else :
851857 analysis ["defaults" ] = {}
852858
853859 # Analyze vars/main.yml
854860 vars_file = role_dir / "vars" / "main.yml"
855861 if vars_file .exists ():
856- try :
857- with open (vars_file , "r" , encoding = "utf-8" ) as f :
858- content = f .read ().strip ()
859- if content : # Only try to parse if file has content
860- vars_data = yaml .safe_load (content )
861- # Handle case where YAML file contains only comments or is empty
862- analysis ["vars" ] = (
863- vars_data if isinstance (vars_data , dict ) else {}
864- )
865- else :
866- analysis ["vars" ] = {}
867- except yaml .YAMLError as e :
868- print (f"Error: Invalid YAML in { vars_file } : { e } " )
869- analysis ["vars" ] = {}
870- except (OSError , IOError ) as e :
871- print (f"Error: Could not read { vars_file } : { e } " )
872- analysis ["vars" ] = {}
873- except Exception as e :
874- print (f"Warning: Could not parse { vars_file } : { e } " )
862+ vars_data = self ._safe_load_yaml_file (vars_file )
863+ if vars_data is not None :
864+ # Handle case where YAML file contains only comments or is empty
865+ analysis ["vars" ] = (
866+ vars_data if isinstance (vars_data , dict ) else {}
867+ )
868+ else :
875869 analysis ["vars" ] = {}
876870
877871 # Analyze meta/main.yml for author information
@@ -887,8 +881,8 @@ def analyze_role_structure(self, role_path: str) -> Dict[str, Any]:
887881 authors = self ._extract_authors_from_meta (meta_data )
888882 analysis ["authors" ] = authors
889883 if authors :
890- print (
891- f" Found { len (authors )} author(s): { ', ' .join (authors )} "
884+ self . log_verbose (
885+ f"Found { len (authors )} author(s): { ', ' .join (authors )} "
892886 )
893887
894888 # Extract description information
@@ -904,23 +898,23 @@ def analyze_role_structure(self, role_path: str) -> Dict[str, Any]:
904898 if descriptions .get ("description" ) or descriptions .get (
905899 "short_description"
906900 ):
907- print (f" Found description from meta/main.yml" )
901+ self . log_verbose (f"Found description from meta/main.yml" )
908902 else :
909903 analysis ["authors" ] = []
910904 analysis ["meta_description" ] = []
911905 analysis ["meta_short_description" ] = ""
912906 except yaml .YAMLError as e :
913- print (f"Warning: Invalid YAML in { meta_file } : { e } " )
907+ self . log_verbose (f"Invalid YAML in { meta_file } : { e } " )
914908 analysis ["authors" ] = []
915909 analysis ["meta_description" ] = []
916910 analysis ["meta_short_description" ] = ""
917911 except (OSError , IOError ) as e :
918- print (f"Warning: Could not read { meta_file } : { e } " )
912+ self . log_verbose (f"Could not read { meta_file } : { e } " )
919913 analysis ["authors" ] = []
920914 analysis ["meta_description" ] = []
921915 analysis ["meta_short_description" ] = ""
922916 except Exception as e :
923- print (f"Warning: Could not parse { meta_file } : { e } " )
917+ self . log_verbose (f"Could not parse { meta_file } : { e } " )
924918 analysis ["authors" ] = []
925919 analysis ["meta_description" ] = []
926920 analysis ["meta_short_description" ] = ""
@@ -1012,7 +1006,7 @@ def _detect_version_info(self, role_dir: Path) -> Dict[str, Any]:
10121006 version_info ["source" ] = "collection"
10131007 return version_info
10141008 except Exception as e :
1015- print (f" Warning: Could not parse galaxy.yml: { e } " )
1009+ self . log_verbose (f"Could not parse galaxy.yml: { e } " )
10161010
10171011 # Not in a collection or no collection version found, check role version
10181012 meta_file = role_dir / "meta" / "main.yml"
@@ -1038,7 +1032,7 @@ def _detect_version_info(self, role_dir: Path) -> Dict[str, Any]:
10381032 version_info ["source" ] = "role"
10391033 return version_info
10401034 except Exception as e :
1041- print (f" Warning: Could not parse meta/main.yml for version: { e } " )
1035+ self . log_verbose (f"Could not parse meta/main.yml for version: { e } " )
10421036
10431037 return version_info
10441038
@@ -1814,25 +1808,13 @@ def ignore_aliases(self, data):
18141808 # Disable YAML references/anchors (like *id001) for cleaner output
18151809 return True
18161810
1817- def generate_anchor (self , node ):
1818- # Prevent anchor generation entirely
1819- return None
1820-
18211811 def ignore_aliases (self , data ):
18221812 # Disable YAML references/anchors (like *id001) for cleaner output
18231813 return True
18241814
1825- def generate_anchor (self , node ):
1826- # Prevent anchor generation entirely
1827- return None
1828-
1829- # Deep copy the specs to avoid any shared object references that could create anchors
1830- import copy
1831- specs_copy = copy .deepcopy (specs )
1832-
18331815 # Configure YAML output for better readability
18341816 yaml_content = yaml .dump (
1835- specs_copy ,
1817+ specs ,
18361818 Dumper = CustomDumper ,
18371819 default_flow_style = False ,
18381820 sort_keys = False ,
@@ -2507,24 +2489,24 @@ def validate_specs(self) -> bool:
25072489 valid = True
25082490
25092491 for entry_name , entry_point in self .entry_points .items ():
2510- print (f"Validating entry point: { entry_name } " )
2492+ self . log_info (f"Validating entry point: { entry_name } " )
25112493
25122494 # Check for required fields
25132495 if not entry_point .short_description :
2514- print (f" Warning: No short_description for { entry_name } " )
2496+ self . log_verbose (f"No short_description for { entry_name } " )
25152497
25162498 # Validate argument types
25172499 for arg_name , arg_spec in entry_point .options .items ():
25182500 if arg_spec .type not in [t .value for t in ArgumentType ]:
2519- print (
2520- f" Error: Invalid type '{ arg_spec .type } ' for argument '{ arg_name } '"
2501+ self . log_error (
2502+ f"Invalid type '{ arg_spec .type } ' for argument '{ arg_name } '"
25212503 )
25222504 valid = False
25232505
25242506 # Check list/dict element types
25252507 if arg_spec .type in ["list" , "dict" ] and not arg_spec .elements :
2526- print (
2527- f" Warning: No elements type specified for { arg_spec .type } argument '{ arg_name } '"
2508+ self . log_verbose (
2509+ f"No elements type specified for { arg_spec .type } argument '{ arg_name } '"
25282510 )
25292511
25302512 # Validate conditionals reference existing arguments
@@ -2548,23 +2530,23 @@ def validate_specs(self) -> bool:
25482530 )
25492531
25502532 if param not in all_args :
2551- print (
2552- f" Error: { condition_type } references unknown argument '{ param } '"
2533+ self . log_error (
2534+ f"{ condition_type } references unknown argument '{ param } '"
25532535 )
25542536 valid = False
25552537
25562538 for req_param in required_params :
25572539 if req_param not in all_args :
2558- print (
2559- f" Error: { condition_type } references unknown argument '{ req_param } '"
2540+ self . log_error (
2541+ f"{ condition_type } references unknown argument '{ req_param } '"
25602542 )
25612543 valid = False
25622544 else :
25632545 # Format: [param1, param2, ...]
25642546 for param in condition :
25652547 if param not in all_args :
2566- print (
2567- f" Error: { condition_type } references unknown argument '{ param } '"
2548+ self . log_error (
2549+ f"{ condition_type } references unknown argument '{ param } '"
25682550 )
25692551 valid = False
25702552
@@ -2596,17 +2578,9 @@ def ignore_aliases(self, data):
25962578 # Disable YAML references/anchors (like *id001) for cleaner output
25972579 return True
25982580
2599- def generate_anchor (self , node ):
2600- # Prevent anchor generation entirely
2601- return None
2602-
2603- # Deep copy the specs to avoid any shared object references that could create anchors
2604- import copy
2605- specs_copy = copy .deepcopy (specs )
2606-
26072581 # Configure YAML output for better readability
26082582 yaml_content = yaml .dump (
2609- specs_copy ,
2583+ specs ,
26102584 Dumper = CustomDumper ,
26112585 default_flow_style = False ,
26122586 sort_keys = False ,
@@ -2684,12 +2658,8 @@ def create_example_config():
26842658 }
26852659 }
26862660
2687- # Deep copy to avoid any shared references
2688- import copy
2689- config_copy = copy .deepcopy (example_config )
2690-
26912661 yaml_content = yaml .dump (
2692- config_copy , default_flow_style = False , sort_keys = False , indent = 2
2662+ example_config , default_flow_style = False , sort_keys = False , indent = 2
26932663 )
26942664
26952665 # Remove any YAML anchor/reference lines that may have slipped through
0 commit comments