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
« 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# ///////////////////////////////////////////////////////////////
6"""
7Template processor - Variable substitution processor for EzCompiler templates.
9This module provides utilities for processing templates with variable
10substitution, including methods for config, version, and setup template
11processing with placeholder replacement.
13Utils layer can only use DEBUG and ERROR log levels.
14"""
16from __future__ import annotations
18# ///////////////////////////////////////////////////////////////
19# IMPORTS
20# ///////////////////////////////////////////////////////////////
21# Standard library imports
22import json
23from pathlib import Path
24from typing import Any
26# Local imports
27from ..shared.exceptions.utils.template_exceptions import (
28 TemplateFileWriteError,
29 TemplateSubstitutionError,
30 TemplateValidationError,
31)
33# ///////////////////////////////////////////////////////////////
34# CLASSES
35# ///////////////////////////////////////////////////////////////
38class TemplateProcessor:
39 """
40 Utility class for processing templates with variable substitution.
42 Provides static methods to replace placeholders in templates with
43 actual values from configuration dictionaries. Supports multiple
44 template types and formats.
46 Example:
47 >>> processor = TemplateProcessor()
48 >>> config = {"version": "1.0.0", "project_name": "MyApp"}
49 >>> result = processor.process_config_template(template, config)
50 """
52 # ////////////////////////////////////////////////
53 # MOCKUP GENERATION METHODS
54 # ////////////////////////////////////////////////
56 @staticmethod
57 def create_mockup_config() -> dict[str, Any]:
58 """
59 Create a mockup configuration dictionary with default values.
61 Provides realistic default values that can be used to generate
62 valid JSON/YAML templates without placeholders.
64 Returns:
65 dict[str, Any]: Dictionary with mockup configuration values
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 }
103 @staticmethod
104 def process_template_with_mockup(template: str) -> str:
105 """
106 Process a template using mockup values to create a valid file.
108 Args:
109 template: The template string with placeholders
111 Returns:
112 str: Processed template string with mockup values
114 Raises:
115 TemplateSubstitutionError: If substitution fails
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)
124 # ////////////////////////////////////////////////
125 # VERSION TEMPLATE PROCESSING
126 # ////////////////////////////////////////////////
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.
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
146 Returns:
147 str: Processed template string
149 Raises:
150 TemplateSubstitutionError: If version substitution fails
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])})"
163 # Get current year
164 from datetime import datetime
166 current_year = str(datetime.now().year)
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 }
181 result = template
182 for placeholder, value in replacements.items():
183 result = result.replace(placeholder, str(value))
185 return result
186 except Exception as e:
187 raise TemplateSubstitutionError(
188 f"Failed to process version template: {e}"
189 ) from e
191 # ////////////////////////////////////////////////
192 # CONFIG TEMPLATE PROCESSING
193 # ////////////////////////////////////////////////
195 @staticmethod
196 def process_config_template(template: str, config: dict[str, Any]) -> str:
197 """
198 Process configuration template with project configuration.
200 Args:
201 template: The configuration template string
202 config: Project configuration dictionary
204 Returns:
205 str: Processed template string
207 Raises:
208 TemplateSubstitutionError: If config substitution fails
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")
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", [])
234 # Create other lists
235 packages = config.get("packages", [])
236 includes = config.get("includes", [])
237 excludes = config.get("excludes", ["debugpy", "test", "unittest"])
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)
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", "")
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)
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 }
286 result = template
287 for placeholder, value in replacements.items():
288 result = result.replace(placeholder, str(value))
290 return result
291 except Exception as e:
292 raise TemplateSubstitutionError(
293 f"Failed to process config template: {e}"
294 ) from e
296 # ////////////////////////////////////////////////
297 # SETUP TEMPLATE PROCESSING
298 # ////////////////////////////////////////////////
300 @staticmethod
301 def process_setup_template(template: str, config: dict[str, Any]) -> str:
302 """
303 Process setup template with project configuration.
305 Args:
306 template: The setup template string
307 config: Project configuration dictionary
309 Returns:
310 str: Processed template string
312 Raises:
313 TemplateSubstitutionError: If setup substitution fails
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")
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 )
340 # Create other lists
341 packages = config.get("packages", [])
342 includes = config.get("includes", [])
343 excludes = config.get("excludes", ["debugpy", "test", "unittest"])
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 }
364 result = template
365 for placeholder, value in replacements.items():
366 result = result.replace(placeholder, str(value))
368 return result
369 except Exception as e:
370 raise TemplateSubstitutionError(
371 f"Failed to process setup template: {e}"
372 ) from e
374 # ////////////////////////////////////////////////
375 # FILE CREATION METHODS
376 # ////////////////////////////////////////////////
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.
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
390 Raises:
391 TemplateFileWriteError: If writing the file fails
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
405 # ////////////////////////////////////////////////
406 # VALIDATION METHODS
407 # ////////////////////////////////////////////////
409 @staticmethod
410 def validate_template(template: str) -> bool:
411 """
412 Validate template syntax.
414 Args:
415 template: Template string to validate
417 Returns:
418 bool: True if template is valid
420 Raises:
421 TemplateValidationError: If template syntax is invalid
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")
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")
437 return True
438 except TemplateValidationError:
439 raise
440 except Exception as e:
441 raise TemplateValidationError(f"Template validation failed: {e}") from e