Coverage for src/ezcompiler/services/compiler_service.py: 94.67%
61 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# 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, cast
26# Third-party imports
27from InquirerPy.resolver import prompt
29from .._types import CompilerPort
31# Local imports
32from ..adapters import CompilerFactory
33from ..shared import CompilationResult, CompilerConfig
34from ..shared.exceptions import CompilationError, ConfigurationError
35from ..utils import ZipUtils
36from ..utils.validators import validate_compiler_name
38# ///////////////////////////////////////////////////////////////
39# TYPE ALIASES
40# ///////////////////////////////////////////////////////////////
42_CompilerName = Literal["Cx_Freeze", "PyInstaller", "Nuitka"]
44# ///////////////////////////////////////////////////////////////
45# CLASSES
46# ///////////////////////////////////////////////////////////////
49class CompilerService:
50 """
51 Compilation orchestration service.
53 Orchestrates project compilation using different compiler backends.
54 Handles compiler selection, validation, and execution.
56 Attributes:
57 _config: CompilerConfig instance with project settings
59 Example:
60 >>> config = CompilerConfig(...)
61 >>> service = CompilerService(config)
62 >>> result = service.compile(console=True, compiler="PyInstaller")
63 >>> print(result.zip_needed)
64 False
65 """
67 # ////////////////////////////////////////////////
68 # INITIALIZATION
69 # ////////////////////////////////////////////////
71 def __init__(self, config: CompilerConfig) -> None:
72 """
73 Initialize the compiler service.
75 Args:
76 config: CompilerConfig instance with project settings
78 Raises:
79 ConfigurationError: If config is None or invalid
80 """
81 if not config:
82 raise ConfigurationError("CompilerConfig is required")
84 self._config = config
85 self._compiler_instance: CompilerPort | None = None
87 # ////////////////////////////////////////////////
88 # COMPILATION METHODS
89 # ////////////////////////////////////////////////
91 def compile(
92 self,
93 console: bool = True,
94 compiler: _CompilerName | None = None,
95 ) -> CompilationResult:
96 """
97 Compile the project using specified or auto-selected compiler.
99 Validates configuration, selects compiler if not specified, and
100 executes compilation. Returns result with zip_needed flag.
102 Args:
103 console: Whether to show console window (default: True)
104 compiler: Compiler to use or None for auto-selection
105 - "Cx_Freeze": Creates directory with dependencies
106 - "PyInstaller": Creates single executable
107 - "Nuitka": Creates standalone folder or single executable
108 - None: Use config compiler, or prompt user if none set
110 Returns:
111 CompilationResult: Result with zip_needed flag and compiler instance
113 Raises:
114 ConfigurationError: If project not initialized
115 CompilationError: If compilation fails
117 Example:
118 >>> service = CompilerService(config)
119 >>> result = service.compile(console=False, compiler="PyInstaller")
120 >>> if result.zip_needed:
121 ... # Create ZIP archive
122 """
123 try:
124 # Determine compiler choice
125 compiler_choice = self._determine_compiler(compiler)
127 # Validate compiler choice
128 if not validate_compiler_name(compiler_choice):
129 raise CompilationError(f"Invalid compiler: {compiler_choice}")
131 # Ensure output directory exists (moved out of CompilerConfig to avoid side effects)
132 self._config.output_folder.mkdir(parents=True, exist_ok=True)
134 # Create and execute compiler
135 self._compiler_instance = self._create_compiler(compiler_choice)
136 self._compiler_instance.compile(console=console)
138 return CompilationResult(
139 zip_needed=self._compiler_instance.zip_needed,
140 compiler_name=compiler_choice,
141 compiler_instance=self._compiler_instance,
142 )
143 except CompilationError:
144 raise
145 except ConfigurationError:
146 raise
147 except Exception as e:
148 raise CompilationError(f"Compilation failed: {str(e)}") from e
150 # ////////////////////////////////////////////////
151 # PRIVATE HELPER METHODS
152 # ////////////////////////////////////////////////
154 def _determine_compiler(self, compiler: _CompilerName | None) -> str:
155 """
156 Determine which compiler to use.
158 Args:
159 compiler: Explicit compiler choice or None/auto
161 Returns:
162 str: Compiler name to use
164 Note:
165 Priority: explicit choice > config.compiler > interactive prompt
166 """
167 # Use explicit choice if provided
168 if compiler:
169 return compiler
171 # Use config compiler if set
172 if self._config.compiler: 172 ↛ 176line 172 didn't jump to line 176 because the condition on line 172 was always true
173 return self._config.compiler
175 # Interactive prompt for user choice
176 return self._choose_compiler_interactively()
178 def _choose_compiler_interactively(
179 self, argv_flags: list[str] | None = None
180 ) -> str:
181 """
182 Prompt user to choose a compiler interactively.
184 Checks command-line flags first, then prompts if needed.
186 Args:
187 argv_flags: List of CLI flags to check. Defaults to sys.argv.
188 Inject a custom list in tests to avoid reading global state.
190 Returns:
191 str: Chosen compiler name
193 Raises:
194 CompilationError: If selection fails
195 """
196 flags = argv_flags if argv_flags is not None else sys.argv
197 try:
198 # Check command line arguments first
199 if "-cxf" in flags:
200 return "Cx_Freeze"
201 elif "-pyi" in flags:
202 return "PyInstaller"
203 elif "-nka" in flags:
204 return "Nuitka"
206 # Prompt user for choice
207 questions = [
208 {
209 "type": "list",
210 "name": "compiler",
211 "message": "Which compiler to use?",
212 "choices": ["Cx_Freeze", "PyInstaller", "Nuitka"],
213 "default": "Cx_Freeze",
214 }
215 ]
217 result = prompt(questions)
218 return cast(str, result["compiler"])
220 except Exception as e:
221 raise CompilationError(f"Failed to choose compiler: {e}") from e
223 def _create_compiler(self, compiler_name: str) -> CompilerPort:
224 """
225 Create compiler instance for the specified compiler.
227 Args:
228 compiler_name: Name of the compiler to create
230 Returns:
231 CompilerPort: Compiler instance
233 Raises:
234 CompilationError: If compiler name is unsupported
235 """
236 return CompilerFactory.create_compiler(
237 config=self._config,
238 compiler_name=compiler_name,
239 )
241 def _zip_artifact(
242 self,
243 output_path: str | Path,
244 progress_callback: Callable[[str, int], None] | None = None,
245 ) -> None:
246 """
247 Create ZIP archive of the compiled output.
249 Args:
250 output_path: Path for the output ZIP file
251 progress_callback: Optional callback(filename, progress) for progress updates
253 Raises:
254 ZipError: If ZIP creation fails
255 """
256 ZipUtils.create_zip_archive(
257 source_path=self._config.output_folder,
258 output_path=output_path,
259 progress_callback=progress_callback,
260 )
262 # ////////////////////////////////////////////////
263 # PROPERTIES
264 # ////////////////////////////////////////////////
266 @property
267 def compiler_instance(self) -> CompilerPort | None:
268 """
269 Get the current compiler instance.
271 Returns:
272 CompilerPort | None: Current compiler instance or None if not compiled yet
273 """
274 return self._compiler_instance