Coverage for src/ezcompiler/utils/_template_utils.py: 93.70%
115 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +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 import COMPILER_SECTION_KEYS
28from ..shared.exceptions.utils import (
29 TemplateFileWriteError,
30 TemplateSubstitutionError,
31 TemplateValidationError,
32)
34# ///////////////////////////////////////////////////////////////
35# CLASSES
36# ///////////////////////////////////////////////////////////////
39class TemplateProcessor:
40 """
41 Utility class for processing templates with variable substitution.
43 Provides static methods to replace placeholders in templates with
44 actual values from configuration dictionaries. Supports multiple
45 template types and formats.
47 Example:
48 >>> processor = TemplateProcessor()
49 >>> config = {"version": "1.0.0", "project_name": "MyApp"}
50 >>> result = processor.process_config_template(template, config)
51 """
53 # ////////////////////////////////////////////////
54 # MOCKUP GENERATION METHODS
55 # ////////////////////////////////////////////////
57 @staticmethod
58 def create_mockup_config() -> dict[str, Any]:
59 """
60 Create a mockup configuration dictionary with default values.
62 Provides realistic default values that can be used to generate
63 valid JSON/YAML templates without placeholders.
65 Returns:
66 dict[str, Any]: Dictionary with mockup configuration values
68 Example:
69 >>> mockup = TemplateProcessor.create_mockup_config()
70 >>> print(mockup["version"])
71 '1.0.0'
72 """
73 return {
74 "version": "1.0.0",
75 "project_name": "MyProject",
76 "project_description": "A sample project description",
77 "company_name": "MyCompany",
78 "author": "John Doe",
79 "main_file": "main.py",
80 "icon": "icon.ico",
81 "version_filename": "version_info.txt",
82 "output_folder": "dist",
83 "include_files": {
84 "files": ["config.yaml", "README.md", "requirements.txt"],
85 "folders": ["assets", "data", "docs"],
86 },
87 "packages": ["requests", "pyyaml", "click"],
88 "includes": ["utils", "helpers", "config"],
89 "excludes": ["test", "debug", "temp"],
90 "compilation": {
91 "console": True,
92 "compiler": "PyInstaller",
93 },
94 "upload": {
95 "repo_destination": "disk",
96 "repo_endpoint": "releases",
97 "release_destination": "disk",
98 "release_endpoint": "",
99 },
100 "pyinstaller": {"optimize": True, "strip": False},
101 "advanced": {"debug": False},
102 }
104 @staticmethod
105 def process_template_with_mockup(template: str) -> str:
106 """
107 Process a template using mockup values to create a valid file.
109 Args:
110 template: The template string with placeholders
112 Returns:
113 str: Processed template string with mockup values
115 Raises:
116 TemplateSubstitutionError: If substitution fails
118 Example:
119 >>> template = "version: #VERSION#\\nproject: #PROJECT_NAME#"
120 >>> result = TemplateProcessor.process_template_with_mockup(template)
121 """
122 mockup_config = TemplateProcessor.create_mockup_config()
123 return TemplateProcessor.process_config_template(template, mockup_config)
125 # ////////////////////////////////////////////////
126 # VERSION TEMPLATE PROCESSING
127 # ////////////////////////////////////////////////
129 @staticmethod
130 def process_version_template(
131 template: str,
132 version: str,
133 company_name: str,
134 project_description: str,
135 project_name: str,
136 ) -> str:
137 """
138 Process version info template with project-specific values.
140 Args:
141 template: The version template string
142 version: Project version (e.g., "1.0.0")
143 company_name: Company name
144 project_description: Project description
145 project_name: Project name
147 Returns:
148 str: Processed template string
150 Raises:
151 TemplateSubstitutionError: If version substitution fails
153 Note:
154 Converts version string to tuple format (e.g., "1.0.0" -> "(1, 0, 0, 0)")
155 and automatically includes current year for copyright.
156 """
157 try:
158 # Convert version string to tuple format
159 version_parts = version.split(".")
160 while len(version_parts) < 4:
161 version_parts.append("0")
162 fixed_version = f"({', '.join(version_parts[:4])})"
164 # Get current year
165 from datetime import datetime
167 current_year = str(datetime.now().year)
169 # Replace placeholders
170 replacements = {
171 "#FIXED_VERSION#": fixed_version,
172 "#STRING_VERSION#": version,
173 "#COMPANY_NAME#": company_name,
174 "#FILE_DESCRIPTION#": project_description,
175 "#PRODUCT_NAME#": project_name,
176 "#INTERNAL_NAME#": project_name,
177 "#LEGAL_COPYRIGHT#": company_name,
178 "#ORIGINAL_FILENAME#": project_name,
179 "#YEAR#": current_year,
180 }
182 result = template
183 for placeholder, value in replacements.items():
184 result = result.replace(placeholder, str(value))
186 return result
187 except Exception as e:
188 raise TemplateSubstitutionError(
189 f"Failed to process version template: {e}"
190 ) from e
192 # ////////////////////////////////////////////////
193 # CONFIG TEMPLATE PROCESSING
194 # ////////////////////////////////////////////////
196 @staticmethod
197 def process_config_template(template: str, config: dict[str, Any]) -> str:
198 """
199 Process configuration template with project configuration.
201 Args:
202 template: The configuration template string
203 config: Project configuration dictionary
205 Returns:
206 str: Processed template string
208 Raises:
209 TemplateSubstitutionError: If config substitution fails
211 Note:
212 Handles nested dictionaries for include_files, compilation,
213 upload, and advanced settings. Converts Python booleans to
214 JSON-compatible lowercase strings.
215 """
216 try:
217 # Extract values from config with defaults
218 version = config.get("version", "1.0.0")
219 project_name = config.get("project_name", "MyProject")
220 project_description = config.get(
221 "project_description", "Project Description"
222 )
223 company_name = config.get("company_name", "Company Name")
224 author = config.get("author", "Author Name")
225 icon = config.get("icon", "icon.ico")
226 main_file = config.get("main_file", "main.py")
227 version_file = config.get("version_filename", "version_info.txt")
228 output_folder = config.get("output_folder", "dist")
230 # Create include_files lists
231 include_files = config.get("include_files", {"files": [], "folders": []})
232 include_files_list = include_files.get("files", [])
233 include_folders_list = include_files.get("folders", [])
235 # Create other lists
236 packages = config.get("packages", [])
237 includes = config.get("includes", [])
238 excludes = config.get("excludes", ["debugpy", "test", "unittest"])
240 # Compilation options
241 compilation = config.get("compilation", {})
242 console = compilation.get("console", True)
243 compiler = compilation.get("compiler") or "PyInstaller"
244 compiler_key = COMPILER_SECTION_KEYS.get(compiler, "pyinstaller")
245 # Upload options
246 upload = config.get("upload", {})
247 repo_destination = upload.get("repo_destination", "disk")
248 repo_endpoint = upload.get("repo_endpoint", "")
249 release_destination = upload.get("release_destination", "disk")
250 release_endpoint = upload.get("release_endpoint", "")
251 repo_public_url = upload.get("repo_public_url", "")
253 # Advanced options (generic)
254 advanced = config.get("advanced", {})
255 debug = advanced.get("debug", False)
257 # Compiler-specific options: read from the compiler section, then
258 # top-level, then defaults.
259 section = config.get(compiler_key, {})
260 optimize = section.get("optimize", config.get("optimize", True))
261 strip = section.get("strip", config.get("strip", False))
263 # Replace placeholders with JSON-valid values
264 replacements = {
265 "#VERSION#": version,
266 "#PROJECT_NAME#": project_name,
267 "#PROJECT_DESCRIPTION#": project_description,
268 "#COMPANY_NAME#": company_name,
269 "#AUTHOR#": author,
270 "#ICON#": icon,
271 "#MAIN_FILE#": main_file,
272 "#VERSION_FILE#": version_file,
273 "#OUTPUT_FOLDER#": output_folder,
274 "#INCLUDE_FILES#": json.dumps(include_files_list),
275 "#INCLUDE_FOLDERS#": json.dumps(include_folders_list),
276 "#PACKAGES#": json.dumps(packages),
277 "#INCLUDES#": json.dumps(includes),
278 "#EXCLUDES#": json.dumps(excludes),
279 "#CONSOLE#": str(console).lower(),
280 "#COMPILER#": compiler,
281 "#COMPILER_KEY#": compiler_key,
282 "#REPO_DESTINATION#": repo_destination,
283 "#REPO_ENDPOINT#": repo_endpoint,
284 "#RELEASE_DESTINATION#": release_destination,
285 "#RELEASE_ENDPOINT#": release_endpoint,
286 "#REPO_PUBLIC_URL#": repo_public_url,
287 "#OPTIMIZE#": str(optimize).lower(),
288 "#STRIP#": str(strip).lower(),
289 "#DEBUG#": str(debug).lower(),
290 }
292 result = template
293 for placeholder, value in replacements.items():
294 result = result.replace(placeholder, str(value))
296 return result
297 except Exception as e:
298 raise TemplateSubstitutionError(
299 f"Failed to process config template: {e}"
300 ) from e
302 # ////////////////////////////////////////////////
303 # SETUP TEMPLATE PROCESSING
304 # ////////////////////////////////////////////////
306 @staticmethod
307 def process_setup_template(template: str, config: dict[str, Any]) -> str:
308 """
309 Process setup template with project configuration.
311 Args:
312 template: The setup template string
313 config: Project configuration dictionary
315 Returns:
316 str: Processed template string
318 Raises:
319 TemplateSubstitutionError: If setup substitution fails
321 Note:
322 Formats include_files as Python dict string representation
323 for setup.py compatibility.
324 """
325 try:
326 # Extract values from config with defaults
327 version = config.get("version", "1.0.0")
328 project_name = config.get("project_name", "MyProject")
329 project_description = config.get(
330 "project_description", "Project Description"
331 )
332 company_name = config.get("company_name", "Company Name")
333 author = config.get("author", "Author Name")
334 icon = config.get("icon", "icon.ico")
335 main_file = config.get("main_file", "main.py")
336 version_file = config.get("version_filename", "version_info.txt")
337 output_folder = config.get("output_folder", "dist")
339 # Create include_files dict string representation
340 include_files = config.get("include_files", {"files": [], "folders": []})
341 include_files_str = (
342 f'{{"files": {include_files.get("files", [])}, '
343 f'"folders": {include_files.get("folders", [])}}}'
344 )
346 # Create other lists
347 packages = config.get("packages", [])
348 includes = config.get("includes", [])
349 excludes = config.get("excludes", ["debugpy", "test", "unittest"])
351 # Replace placeholders
352 replacements = {
353 "#VERSION#": version,
354 "#PROJECT_NAME#": project_name,
355 "#PROJECT_DESCRIPTION#": project_description,
356 "#COMPANY_NAME#": company_name,
357 "#AUTHOR#": author,
358 "#ICON#": icon,
359 "#MAIN_FILE#": main_file,
360 "#VERSION_FILE#": version_file,
361 "#OUTPUT_FOLDER#": output_folder,
362 "#INCLUDE_FILES#": include_files_str,
363 "#PACKAGES#": str(packages),
364 "#INCLUDES#": str(includes),
365 "#EXCLUDES#": str(excludes),
366 }
368 result = template
369 for placeholder, value in replacements.items():
370 result = result.replace(placeholder, str(value))
372 return result
373 except Exception as e:
374 raise TemplateSubstitutionError(
375 f"Failed to process setup template: {e}"
376 ) from e
378 # ////////////////////////////////////////////////
379 # FILE CREATION METHODS
380 # ////////////////////////////////////////////////
382 @staticmethod
383 def _create_config_file(
384 template: str, _config: dict[str, Any], output_path: Path
385 ) -> None:
386 """
387 Create a configuration file from template.
389 Args:
390 template: The configuration template
391 _config: Configuration values to substitute (currently unused)
392 output_path: Path where to save the config file
394 Raises:
395 TemplateFileWriteError: If writing the file fails
397 Note:
398 Currently writes template as-is. Future versions may process
399 the template with config values.
400 """
401 try:
402 with open(output_path, "w", encoding="utf-8") as f:
403 f.write(template)
404 except Exception as e:
405 raise TemplateFileWriteError(
406 f"Failed to write config file to {output_path}: {e}"
407 ) from e
409 # ////////////////////////////////////////////////
410 # VALIDATION METHODS
411 # ////////////////////////////////////////////////
413 @staticmethod
414 def validate_template(template: str) -> bool:
415 """
416 Validate template syntax.
418 Args:
419 template: Template string to validate
421 Returns:
422 bool: True if template is valid
424 Raises:
425 TemplateValidationError: If template syntax is invalid
427 Note:
428 Performs basic validation: checks for balanced braces and quotes.
429 """
430 try:
431 # Check for balanced braces
432 brace_count = template.count("{") - template.count("}")
433 if brace_count != 0:
434 raise TemplateValidationError("Unbalanced braces in template")
436 # Check for balanced quotes (excluding escaped quotes)
437 quote_count = template.count('"') - template.count('\\"')
438 if quote_count % 2 != 0:
439 raise TemplateValidationError("Unbalanced quotes in template")
441 return True
442 except TemplateValidationError:
443 raise
444 except Exception as e:
445 raise TemplateValidationError(f"Template validation failed: {e}") from e