Coverage for src / ezcompiler / services / compiler_service.py: 94.59%
60 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 00:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-01 00:22 +0000
1# ///////////////////////////////////////////////////////////////
2# COMPILER_SERVICE - Compilation orchestration service
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Compiler service - Compilation orchestration service for EzCompiler.
9This module provides the CompilerService class that orchestrates project
10compilation using different compiler backends (Cx_Freeze, PyInstaller, Nuitka).
12Services layer can use WARNING and ERROR log levels.
13"""
15from __future__ import annotations
17# ///////////////////////////////////////////////////////////////
18# IMPORTS
19# ///////////////////////////////////////////////////////////////
20# Standard library imports
21import sys
22from collections.abc import Callable
23from pathlib import Path
24from typing import Literal
26# Third-party imports
27from InquirerPy.resolver import prompt
29# Local imports
30from ..adapters import (
31 BaseCompiler,
32 CompilerFactory,
33)
34from ..shared import CompilationResult, CompilerConfig
35from ..shared.exceptions import CompilationError, ConfigurationError
36from ..utils import ZipUtils
37from ..utils.validators import validate_compiler_name
39# ///////////////////////////////////////////////////////////////
40# TYPE ALIASES
41# ///////////////////////////////////////////////////////////////
43_CompilerName = Literal["Cx_Freeze", "PyInstaller", "Nuitka", "auto"]
45# ///////////////////////////////////////////////////////////////
46# CLASSES
47# ///////////////////////////////////////////////////////////////
50class CompilerService:
51 """
52 Compilation orchestration service.
54 Orchestrates project compilation using different compiler backends.
55 Handles compiler selection, validation, and execution.
57 Attributes:
58 _config: CompilerConfig instance with project settings
60 Example:
61 >>> config = CompilerConfig(...)
62 >>> service = CompilerService(config)
63 >>> result = service.compile(console=True, compiler="PyInstaller")
64 >>> print(result.zip_needed)
65 False
66 """
68 # ////////////////////////////////////////////////
69 # INITIALIZATION
70 # ////////////////////////////////////////////////
72 def __init__(self, config: CompilerConfig) -> None:
73 """
74 Initialize the compiler service.
76 Args:
77 config: CompilerConfig instance with project settings
79 Raises:
80 ConfigurationError: If config is None or invalid
81 """
82 if not config:
83 raise ConfigurationError("CompilerConfig is required")
85 self._config = config
86 self._compiler_instance: BaseCompiler | None = None
88 # ////////////////////////////////////////////////
89 # COMPILATION METHODS
90 # ////////////////////////////////////////////////
92 def compile(
93 self,
94 console: bool = True,
95 compiler: _CompilerName | None = None,
96 ) -> CompilationResult:
97 """
98 Compile the project using specified or auto-selected compiler.
100 Validates configuration, selects compiler if not specified, and
101 executes compilation. Returns result with zip_needed flag.
103 Args:
104 console: Whether to show console window (default: True)
105 compiler: Compiler to use or None for auto-selection
106 - "Cx_Freeze": Creates directory with dependencies
107 - "PyInstaller": Creates single executable
108 - "Nuitka": Creates standalone folder or single executable
109 - "auto" or None: Prompt user for choice or use config default
111 Returns:
112 CompilationResult: Result with zip_needed flag and compiler instance
114 Raises:
115 ConfigurationError: If project not initialized
116 CompilationError: If compilation fails
118 Example:
119 >>> service = CompilerService(config)
120 >>> result = service.compile(console=False, compiler="PyInstaller")
121 >>> if result.zip_needed:
122 ... # Create ZIP archive
123 """
124 try:
125 # Determine compiler choice
126 compiler_choice = self._determine_compiler(compiler)
128 # Validate compiler choice
129 if not validate_compiler_name(compiler_choice):
130 raise CompilationError(f"Invalid compiler: {compiler_choice}")
132 # Ensure output directory exists (moved out of CompilerConfig to avoid side effects)
133 self._config.output_folder.mkdir(parents=True, exist_ok=True)
135 # Create and execute compiler
136 self._compiler_instance = self._create_compiler(compiler_choice)
137 self._compiler_instance.compile(console=console)
139 return CompilationResult(
140 zip_needed=self._compiler_instance.zip_needed,
141 compiler_name=compiler_choice,
142 compiler_instance=self._compiler_instance,
143 )
144 except CompilationError:
145 raise
146 except ConfigurationError:
147 raise
148 except Exception as e:
149 raise CompilationError(f"Compilation failed: {str(e)}") from e
151 # ////////////////////////////////////////////////
152 # PRIVATE HELPER METHODS
153 # ////////////////////////////////////////////////
155 def _determine_compiler(self, compiler: _CompilerName | None) -> str:
156 """
157 Determine which compiler to use.
159 Args:
160 compiler: Explicit compiler choice or None/auto
162 Returns:
163 str: Compiler name to use
165 Note:
166 Priority: explicit choice > config.compiler > interactive prompt
167 """
168 # Use explicit choice if provided
169 if compiler and compiler != "auto":
170 return compiler
172 # Use config default if set and not auto
173 if self._config.compiler and self._config.compiler != "auto": 173 ↛ 177line 173 didn't jump to line 177 because the condition on line 173 was always true
174 return self._config.compiler
176 # Interactive prompt for user choice
177 return self._choose_compiler_interactively()
179 def _choose_compiler_interactively(
180 self, argv_flags: list[str] | None = None
181 ) -> str:
182 """
183 Prompt user to choose a compiler interactively.
185 Checks command-line flags first, then prompts if needed.
187 Args:
188 argv_flags: List of CLI flags to check. Defaults to sys.argv.
189 Inject a custom list in tests to avoid reading global state.
191 Returns:
192 str: Chosen compiler name
194 Raises:
195 CompilationError: If selection fails
196 """
197 flags = argv_flags if argv_flags is not None else sys.argv
198 try:
199 # Check command line arguments first
200 if "-cxf" in flags:
201 return "Cx_Freeze"
202 elif "-pyi" in flags:
203 return "PyInstaller"
204 elif "-nka" in flags:
205 return "Nuitka"
207 # Prompt user for choice
208 questions = [
209 {
210 "type": "list",
211 "name": "compiler",
212 "message": "Which compiler to use?",
213 "choices": ["Cx_Freeze", "PyInstaller", "Nuitka"],
214 "default": "Cx_Freeze",
215 }
216 ]
218 result = prompt(questions)
219 return result["compiler"] # type: ignore[return-value]
221 except Exception as e:
222 raise CompilationError(f"Failed to choose compiler: {e}") from e
224 def _create_compiler(self, compiler_name: str) -> BaseCompiler:
225 """
226 Create compiler instance for the specified compiler.
228 Args:
229 compiler_name: Name of the compiler to create
231 Returns:
232 BaseCompiler: Compiler instance
234 Raises:
235 CompilationError: If compiler name is unsupported
236 """
237 return CompilerFactory.create_compiler(
238 config=self._config,
239 compiler_name=compiler_name,
240 )
242 def _zip_artifact(
243 self,
244 output_path: str | Path,
245 progress_callback: Callable[[str, int], None] | None = None,
246 ) -> None:
247 """
248 Create ZIP archive of the compiled output.
250 Args:
251 output_path: Path for the output ZIP file
252 progress_callback: Optional callback(filename, progress) for progress updates
254 Raises:
255 ZipError: If ZIP creation fails
256 """
257 ZipUtils.create_zip_archive(
258 source_path=self._config.output_folder,
259 output_path=output_path,
260 progress_callback=progress_callback,
261 )
263 # ////////////////////////////////////////////////
264 # PROPERTIES
265 # ////////////////////////////////////////////////
267 @property
268 def compiler_instance(self) -> BaseCompiler | None:
269 """
270 Get the current compiler instance.
272 Returns:
273 BaseCompiler | None: Current compiler instance or None if not compiled yet
274 """
275 return self._compiler_instance