Coverage for src/c2puml/main.py: 62%
120 statements
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 03:53 +0000
« prev ^ index » next coverage.py v7.10.4, created at 2025-08-20 03:53 +0000
1#!/usr/bin/env python3
2"""
3Main entry point for C to PlantUML converter
5Processing Flow:
61. Parse C/C++ files and generate model.json
72. Transform model based on configuration
83. Generate PlantUML files from the transformed model
9"""
11import argparse
12import json
13import logging
14import os
15import sys
16from pathlib import Path
18from .config import Config
19from .core.generator import Generator
20from .core.parser import Parser
21from .core.transformer import Transformer
24def setup_logging(verbose: bool = False) -> None:
25 level = logging.DEBUG if verbose else logging.INFO
26 logging.basicConfig(
27 level=level,
28 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
29 handlers=[logging.StreamHandler(sys.stdout)],
30 )
33def load_config_from_path(config_path: str) -> dict:
34 path = Path(config_path)
35 if path.is_file():
36 with open(path, "r", encoding="utf-8") as f:
37 return json.load(f)
38 elif path.is_dir():
39 # Merge all .json files in the folder
40 config = {}
41 for file in path.glob("*.json"):
42 with open(file, "r", encoding="utf-8") as f:
43 data = json.load(f)
44 config.update(data)
45 return config
46 else:
47 raise FileNotFoundError(f"Config path not found: {config_path}")
50def main() -> int:
51 parser = argparse.ArgumentParser(
52 description="C to PlantUML Converter (Simplified CLI)",
53 formatter_class=argparse.RawDescriptionHelpFormatter,
54 epilog="""
55Usage:
56 %(prog)s --config config.json [parse|transform|generate]
57 %(prog)s config_folder [parse|transform|generate]
58 %(prog)s [parse|transform|generate] # Uses current directory as config folder
59 %(prog)s # Full workflow (parse, transform, generate)
60 """,
61 )
62 parser.add_argument(
63 "--config",
64 "-c",
65 type=str,
66 default=None,
67 help="Path to config.json or config folder (optional, default: current directory)",
68 )
69 parser.add_argument(
70 "command",
71 nargs="?",
72 choices=["parse", "transform", "generate"],
73 help="Which step to run: parse, transform, or generate. If omitted, runs full workflow.",
74 )
75 parser.add_argument(
76 "--verbose", "-v", action="store_true", help="Enable verbose output"
77 )
78 args = parser.parse_args()
80 setup_logging(args.verbose)
82 # Determine config path
83 config_path = args.config
84 if config_path is None:
85 config_path = os.getcwd()
87 logging.info("Using config: %s", config_path)
89 # Load config
90 try:
91 config_data = load_config_from_path(config_path)
92 config = Config(**config_data)
93 except Exception as e:
94 logging.error("Failed to load configuration: %s", e)
95 return 1
97 # Determine output folder from config, default to ./output
98 output_folder = getattr(config, "output_dir", None) or os.path.join(
99 os.getcwd(), "output"
100 )
101 output_folder = os.path.abspath(output_folder)
102 os.makedirs(output_folder, exist_ok=True)
103 logging.info("Output folder: %s", output_folder)
105 model_file = os.path.join(output_folder, "model.json")
106 transformed_model_file = os.path.join(output_folder, "model_transformed.json")
108 # Parse command
109 if args.command == "parse":
110 try:
111 parser_obj = Parser()
112 # Use the parse function with multiple source folders
113 parser_obj.parse(
114 source_folders=config.source_folders,
115 output_file=model_file,
116 recursive_search=getattr(config, "recursive_search", True),
117 config=config,
118 )
119 logging.info("Model saved to: %s", model_file)
120 return 0
121 except (OSError, ValueError, RuntimeError) as e:
122 logging.error("Error during parsing: %s", e)
123 # Provide additional context for common issues
124 if "Source folder not found" in str(e):
125 logging.error("Please check that the source_folders in your configuration exist and are accessible.")
126 logging.error("You can use absolute paths or relative paths from the current working directory.")
127 elif "Permission denied" in str(e):
128 logging.error("Please check file permissions for the source folders.")
129 elif "Invalid JSON" in str(e):
130 logging.error("Please check that your configuration file contains valid JSON.")
131 return 1
133 # Transform command
134 if args.command == "transform":
135 try:
136 transformer = Transformer()
137 transformer.transform(
138 model_file=model_file,
139 config_file=(
140 config_path
141 if Path(config_path).is_file()
142 else str(list(Path(config_path).glob("*.json"))[0])
143 ),
144 output_file=transformed_model_file,
145 )
146 logging.info("Transformed model saved to: %s", transformed_model_file)
147 return 0
148 except (OSError, ValueError, RuntimeError) as e:
149 logging.error("Error during transformation: %s", e)
150 return 1
152 # Generate command
153 if args.command == "generate":
154 try:
155 generator = Generator()
156 # Apply config for signature truncation and macro display
157 Generator.max_function_signature_chars = getattr(config, "max_function_signature_chars", 0)
158 Generator.hide_macro_values = getattr(config, "hide_macro_values", False)
159 Generator.convert_empty_class_to_artifact = getattr(config, "convert_empty_class_to_artifact", False)
160 # Prefer transformed model, else fallback to model.json
161 if os.path.exists(transformed_model_file):
162 model_to_use = transformed_model_file
163 elif os.path.exists(model_file):
164 model_to_use = model_file
165 else:
166 logging.error("No model file found for generation.")
167 logging.error("Please run the parse step first to generate a model file.")
168 return 1
169 generator.generate(
170 model_file=model_to_use,
171 output_dir=output_folder,
172 )
173 logging.info("PlantUML generation complete! Output in: %s", output_folder)
174 return 0
175 except (OSError, ValueError, RuntimeError) as e:
176 logging.error("Error generating PlantUML: %s", e)
177 return 1
179 # Default: full workflow
180 try:
181 # Step 1: Parse
182 parser_obj = Parser()
183 # Use the parse function with multiple source folders
184 parser_obj.parse(
185 source_folders=config.source_folders,
186 output_file=model_file,
187 recursive_search=getattr(config, "recursive_search", True),
188 config=config,
189 )
190 logging.info("Model saved to: %s", model_file)
191 # Step 2: Transform
192 transformer = Transformer()
193 transformer.transform(
194 model_file=model_file,
195 config_file=(
196 config_path
197 if Path(config_path).is_file()
198 else str(list(Path(config_path).glob("*.json"))[0])
199 ),
200 output_file=transformed_model_file,
201 )
202 logging.info("Transformed model saved to: %s", transformed_model_file)
203 # Step 3: Generate
204 generator = Generator()
205 # Apply config for signature truncation and macro display
206 Generator.max_function_signature_chars = getattr(config, "max_function_signature_chars", 0)
207 Generator.hide_macro_values = getattr(config, "hide_macro_values", False)
208 Generator.convert_empty_class_to_artifact = getattr(config, "convert_empty_class_to_artifact", False)
209 generator.generate(
210 model_file=transformed_model_file,
211 output_dir=output_folder,
212 )
213 logging.info("PlantUML generation complete! Output in: %s", output_folder)
214 logging.info("Complete workflow finished successfully!")
215 return 0
216 except (OSError, ValueError, RuntimeError) as e:
217 logging.error("Error in workflow: %s", e)
218 # Provide additional context for common issues
219 if "Source folder not found" in str(e):
220 logging.error("Please check that the source_folders in your configuration exist and are accessible.")
221 logging.error("You can use absolute paths or relative paths from the current working directory.")
222 elif "Permission denied" in str(e):
223 logging.error("Please check file permissions for the source folders.")
224 elif "Invalid JSON" in str(e):
225 logging.error("Please check that your configuration file contains valid JSON.")
226 elif "Configuration must contain" in str(e):
227 logging.error("Please check that your configuration file has the required 'source_folders' field.")
228 return 1
231if __name__ == "__main__":
232 sys.exit(main())