Coverage for src / ezcompiler / utils / template_utils.py: 93.55%

112 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-27 06:49 +0000

1# /////////////////////////////////////////////////////////////// 

2# TEMPLATE_UTILS - Template processing utilities 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Template processor - Variable substitution processor for EzCompiler templates. 

8 

9This module provides utilities for processing templates with variable 

10substitution, including methods for config, version, and setup template 

11processing with placeholder replacement. 

12 

13Utils layer can only use DEBUG and ERROR log levels. 

14""" 

15 

16from __future__ import annotations 

17 

18# /////////////////////////////////////////////////////////////// 

19# IMPORTS 

20# /////////////////////////////////////////////////////////////// 

21# Standard library imports 

22import json 

23from pathlib import Path 

24from typing import Any 

25 

26# Local imports 

27from ..shared.exceptions.utils.template_exceptions import ( 

28 TemplateFileWriteError, 

29 TemplateSubstitutionError, 

30 TemplateValidationError, 

31) 

32 

33# /////////////////////////////////////////////////////////////// 

34# CLASSES 

35# /////////////////////////////////////////////////////////////// 

36 

37 

38class TemplateProcessor: 

39 """ 

40 Utility class for processing templates with variable substitution. 

41 

42 Provides static methods to replace placeholders in templates with 

43 actual values from configuration dictionaries. Supports multiple 

44 template types and formats. 

45 

46 Example: 

47 >>> processor = TemplateProcessor() 

48 >>> config = {"version": "1.0.0", "project_name": "MyApp"} 

49 >>> result = processor.process_config_template(template, config) 

50 """ 

51 

52 # //////////////////////////////////////////////// 

53 # MOCKUP GENERATION METHODS 

54 # //////////////////////////////////////////////// 

55 

56 @staticmethod 

57 def create_mockup_config() -> dict[str, Any]: 

58 """ 

59 Create a mockup configuration dictionary with default values. 

60 

61 Provides realistic default values that can be used to generate 

62 valid JSON/YAML templates without placeholders. 

63 

64 Returns: 

65 dict[str, Any]: Dictionary with mockup configuration values 

66 

67 Example: 

68 >>> mockup = TemplateProcessor.create_mockup_config() 

69 >>> print(mockup["version"]) 

70 '1.0.0' 

71 """ 

72 return { 

73 "version": "1.0.0", 

74 "project_name": "MyProject", 

75 "project_description": "A sample project description", 

76 "company_name": "MyCompany", 

77 "author": "John Doe", 

78 "main_file": "main.py", 

79 "icon": "icon.ico", 

80 "version_filename": "version_info.txt", 

81 "output_folder": "dist", 

82 "include_files": { 

83 "files": ["config.yaml", "README.md", "requirements.txt"], 

84 "folders": ["assets", "data", "docs"], 

85 }, 

86 "packages": ["requests", "pyyaml", "click"], 

87 "includes": ["utils", "helpers", "config"], 

88 "excludes": ["test", "debug", "temp"], 

89 "compilation": { 

90 "console": True, 

91 "compiler": "auto", 

92 "zip_needed": True, 

93 "repo_needed": False, 

94 }, 

95 "upload": { 

96 "structure": "disk", 

97 "repo_path": "releases", 

98 "server_url": "https://example.com/upload", 

99 }, 

100 "advanced": {"optimize": True, "strip": False, "debug": False}, 

101 } 

102 

103 @staticmethod 

104 def process_template_with_mockup(template: str) -> str: 

105 """ 

106 Process a template using mockup values to create a valid file. 

107 

108 Args: 

109 template: The template string with placeholders 

110 

111 Returns: 

112 str: Processed template string with mockup values 

113 

114 Raises: 

115 TemplateSubstitutionError: If substitution fails 

116 

117 Example: 

118 >>> template = "version: #VERSION#\\nproject: #PROJECT_NAME#" 

119 >>> result = TemplateProcessor.process_template_with_mockup(template) 

120 """ 

121 mockup_config = TemplateProcessor.create_mockup_config() 

122 return TemplateProcessor.process_config_template(template, mockup_config) 

123 

124 # //////////////////////////////////////////////// 

125 # VERSION TEMPLATE PROCESSING 

126 # //////////////////////////////////////////////// 

127 

128 @staticmethod 

129 def process_version_template( 

130 template: str, 

131 version: str, 

132 company_name: str, 

133 project_description: str, 

134 project_name: str, 

135 ) -> str: 

136 """ 

137 Process version info template with project-specific values. 

138 

139 Args: 

140 template: The version template string 

141 version: Project version (e.g., "1.0.0") 

142 company_name: Company name 

143 project_description: Project description 

144 project_name: Project name 

145 

146 Returns: 

147 str: Processed template string 

148 

149 Raises: 

150 TemplateSubstitutionError: If version substitution fails 

151 

152 Note: 

153 Converts version string to tuple format (e.g., "1.0.0" -> "(1, 0, 0, 0)") 

154 and automatically includes current year for copyright. 

155 """ 

156 try: 

157 # Convert version string to tuple format 

158 version_parts = version.split(".") 

159 while len(version_parts) < 4: 

160 version_parts.append("0") 

161 fixed_version = f"({', '.join(version_parts[:4])})" 

162 

163 # Get current year 

164 from datetime import datetime 

165 

166 current_year = str(datetime.now().year) 

167 

168 # Replace placeholders 

169 replacements = { 

170 "#FIXED_VERSION#": fixed_version, 

171 "#STRING_VERSION#": version, 

172 "#COMPANY_NAME#": company_name, 

173 "#FILE_DESCRIPTION#": project_description, 

174 "#PRODUCT_NAME#": project_name, 

175 "#INTERNAL_NAME#": project_name, 

176 "#LEGAL_COPYRIGHT#": company_name, 

177 "#ORIGINAL_FILENAME#": project_name, 

178 "#YEAR#": current_year, 

179 } 

180 

181 result = template 

182 for placeholder, value in replacements.items(): 

183 result = result.replace(placeholder, str(value)) 

184 

185 return result 

186 except Exception as e: 

187 raise TemplateSubstitutionError( 

188 f"Failed to process version template: {e}" 

189 ) from e 

190 

191 # //////////////////////////////////////////////// 

192 # CONFIG TEMPLATE PROCESSING 

193 # //////////////////////////////////////////////// 

194 

195 @staticmethod 

196 def process_config_template(template: str, config: dict[str, Any]) -> str: 

197 """ 

198 Process configuration template with project configuration. 

199 

200 Args: 

201 template: The configuration template string 

202 config: Project configuration dictionary 

203 

204 Returns: 

205 str: Processed template string 

206 

207 Raises: 

208 TemplateSubstitutionError: If config substitution fails 

209 

210 Note: 

211 Handles nested dictionaries for include_files, compilation, 

212 upload, and advanced settings. Converts Python booleans to 

213 JSON-compatible lowercase strings. 

214 """ 

215 try: 

216 # Extract values from config with defaults 

217 version = config.get("version", "1.0.0") 

218 project_name = config.get("project_name", "MyProject") 

219 project_description = config.get( 

220 "project_description", "Project Description" 

221 ) 

222 company_name = config.get("company_name", "Company Name") 

223 author = config.get("author", "Author Name") 

224 icon = config.get("icon", "icon.ico") 

225 main_file = config.get("main_file", "main.py") 

226 version_file = config.get("version_filename", "version_info.txt") 

227 output_folder = config.get("output_folder", "dist") 

228 

229 # Create include_files lists 

230 include_files = config.get("include_files", {"files": [], "folders": []}) 

231 include_files_list = include_files.get("files", []) 

232 include_folders_list = include_files.get("folders", []) 

233 

234 # Create other lists 

235 packages = config.get("packages", []) 

236 includes = config.get("includes", []) 

237 excludes = config.get("excludes", ["debugpy", "test", "unittest"]) 

238 

239 # Compilation options 

240 compilation = config.get("compilation", {}) 

241 console = compilation.get("console", True) 

242 compiler = compilation.get("compiler", "auto") 

243 zip_needed = compilation.get("zip_needed", True) 

244 repo_needed = compilation.get("repo_needed", False) 

245 

246 # Upload options 

247 upload = config.get("upload", {}) 

248 upload_structure = upload.get("structure", "disk") 

249 repo_path = upload.get("repo_path", "releases") 

250 server_url = upload.get("server_url", "") 

251 

252 # Advanced options 

253 advanced = config.get("advanced", {}) 

254 optimize = advanced.get("optimize", True) 

255 strip = advanced.get("strip", False) 

256 debug = advanced.get("debug", False) 

257 

258 # Replace placeholders with JSON-valid values 

259 replacements = { 

260 "#VERSION#": version, 

261 "#PROJECT_NAME#": project_name, 

262 "#PROJECT_DESCRIPTION#": project_description, 

263 "#COMPANY_NAME#": company_name, 

264 "#AUTHOR#": author, 

265 "#ICON#": icon, 

266 "#MAIN_FILE#": main_file, 

267 "#VERSION_FILE#": version_file, 

268 "#OUTPUT_FOLDER#": output_folder, 

269 "#INCLUDE_FILES#": json.dumps(include_files_list), 

270 "#INCLUDE_FOLDERS#": json.dumps(include_folders_list), 

271 "#PACKAGES#": json.dumps(packages), 

272 "#INCLUDES#": json.dumps(includes), 

273 "#EXCLUDES#": json.dumps(excludes), 

274 "#CONSOLE#": str(console).lower(), 

275 "#COMPILER#": compiler, 

276 "#ZIP_NEEDED#": str(zip_needed).lower(), 

277 "#REPO_NEEDED#": str(repo_needed).lower(), 

278 "#UPLOAD_STRUCTURE#": upload_structure, 

279 "#REPO_PATH#": repo_path, 

280 "#SERVER_URL#": server_url, 

281 "#OPTIMIZE#": str(optimize).lower(), 

282 "#STRIP#": str(strip).lower(), 

283 "#DEBUG#": str(debug).lower(), 

284 } 

285 

286 result = template 

287 for placeholder, value in replacements.items(): 

288 result = result.replace(placeholder, str(value)) 

289 

290 return result 

291 except Exception as e: 

292 raise TemplateSubstitutionError( 

293 f"Failed to process config template: {e}" 

294 ) from e 

295 

296 # //////////////////////////////////////////////// 

297 # SETUP TEMPLATE PROCESSING 

298 # //////////////////////////////////////////////// 

299 

300 @staticmethod 

301 def process_setup_template(template: str, config: dict[str, Any]) -> str: 

302 """ 

303 Process setup template with project configuration. 

304 

305 Args: 

306 template: The setup template string 

307 config: Project configuration dictionary 

308 

309 Returns: 

310 str: Processed template string 

311 

312 Raises: 

313 TemplateSubstitutionError: If setup substitution fails 

314 

315 Note: 

316 Formats include_files as Python dict string representation 

317 for setup.py compatibility. 

318 """ 

319 try: 

320 # Extract values from config with defaults 

321 version = config.get("version", "1.0.0") 

322 project_name = config.get("project_name", "MyProject") 

323 project_description = config.get( 

324 "project_description", "Project Description" 

325 ) 

326 company_name = config.get("company_name", "Company Name") 

327 author = config.get("author", "Author Name") 

328 icon = config.get("icon", "icon.ico") 

329 main_file = config.get("main_file", "main.py") 

330 version_file = config.get("version_filename", "version_info.txt") 

331 output_folder = config.get("output_folder", "dist") 

332 

333 # Create include_files dict string representation 

334 include_files = config.get("include_files", {"files": [], "folders": []}) 

335 include_files_str = ( 

336 f'{{"files": {include_files.get("files", [])}, ' 

337 f'"folders": {include_files.get("folders", [])}}}' 

338 ) 

339 

340 # Create other lists 

341 packages = config.get("packages", []) 

342 includes = config.get("includes", []) 

343 excludes = config.get("excludes", ["debugpy", "test", "unittest"]) 

344 

345 # Replace placeholders 

346 replacements = { 

347 "#VERSION#": version, 

348 "#ZIP_NEEDED#": "True", 

349 "#REPO_NEEDED#": "False", 

350 "#PROJECT_NAME#": project_name, 

351 "#PROJECT_DESCRIPTION#": project_description, 

352 "#COMPANY_NAME#": company_name, 

353 "#AUTHOR#": author, 

354 "#ICON#": icon, 

355 "#MAIN_FILE#": main_file, 

356 "#VERSION_FILE#": version_file, 

357 "#OUTPUT_FOLDER#": output_folder, 

358 "#INCLUDE_FILES#": include_files_str, 

359 "#PACKAGES#": str(packages), 

360 "#INCLUDES#": str(includes), 

361 "#EXCLUDES#": str(excludes), 

362 } 

363 

364 result = template 

365 for placeholder, value in replacements.items(): 

366 result = result.replace(placeholder, str(value)) 

367 

368 return result 

369 except Exception as e: 

370 raise TemplateSubstitutionError( 

371 f"Failed to process setup template: {e}" 

372 ) from e 

373 

374 # //////////////////////////////////////////////// 

375 # FILE CREATION METHODS 

376 # //////////////////////////////////////////////// 

377 

378 @staticmethod 

379 def _create_config_file( 

380 template: str, _config: dict[str, Any], output_path: Path 

381 ) -> None: 

382 """ 

383 Create a configuration file from template. 

384 

385 Args: 

386 template: The configuration template 

387 _config: Configuration values to substitute (currently unused) 

388 output_path: Path where to save the config file 

389 

390 Raises: 

391 TemplateFileWriteError: If writing the file fails 

392 

393 Note: 

394 Currently writes template as-is. Future versions may process 

395 the template with config values. 

396 """ 

397 try: 

398 with open(output_path, "w", encoding="utf-8") as f: 

399 f.write(template) 

400 except Exception as e: 

401 raise TemplateFileWriteError( 

402 f"Failed to write config file to {output_path}: {e}" 

403 ) from e 

404 

405 # //////////////////////////////////////////////// 

406 # VALIDATION METHODS 

407 # //////////////////////////////////////////////// 

408 

409 @staticmethod 

410 def validate_template(template: str) -> bool: 

411 """ 

412 Validate template syntax. 

413 

414 Args: 

415 template: Template string to validate 

416 

417 Returns: 

418 bool: True if template is valid 

419 

420 Raises: 

421 TemplateValidationError: If template syntax is invalid 

422 

423 Note: 

424 Performs basic validation: checks for balanced braces and quotes. 

425 """ 

426 try: 

427 # Check for balanced braces 

428 brace_count = template.count("{") - template.count("}") 

429 if brace_count != 0: 

430 raise TemplateValidationError("Unbalanced braces in template") 

431 

432 # Check for balanced quotes (excluding escaped quotes) 

433 quote_count = template.count('"') - template.count('\\"') 

434 if quote_count % 2 != 0: 

435 raise TemplateValidationError("Unbalanced quotes in template") 

436 

437 return True 

438 except TemplateValidationError: 

439 raise 

440 except Exception as e: 

441 raise TemplateValidationError(f"Template validation failed: {e}") from e