Coverage for src / ezcompiler / utils / file_utils.py: 86.62%

124 statements  

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

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

2# FILE_UTILS - File and directory operation utilities 

3# Project: ezcompiler 

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

5 

6""" 

7File utilities - File and directory operation utilities for EzCompiler. 

8 

9This module provides utility functions for common file operations including 

10path validation, directory creation, file copying, and path manipulation. 

11""" 

12 

13from __future__ import annotations 

14 

15# /////////////////////////////////////////////////////////////// 

16# IMPORTS 

17# /////////////////////////////////////////////////////////////// 

18# Standard library imports 

19import shutil 

20from pathlib import Path 

21 

22# Local imports 

23from ..shared.exceptions.utils import ( 

24 DirectoryCreationError, 

25 DirectoryListError, 

26 FileAccessError, 

27 FileCopyError, 

28 FileDeleteError, 

29 FileMoveError, 

30 FileNotFoundError, 

31) 

32 

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

34# CLASSES 

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

36 

37 

38class FileUtils: 

39 """ 

40 File and directory operations utility class. 

41 

42 Provides static methods for common file and directory operations 

43 used throughout the EzCompiler project, including validation, 

44 creation, copying, moving, and listing operations. 

45 

46 Example: 

47 >>> FileUtils.create_directory_if_not_exists("./output") 

48 >>> file_size = FileUtils.get_file_size("path/to/file.txt") 

49 """ 

50 

51 # //////////////////////////////////////////////// 

52 # VALIDATION METHODS 

53 # //////////////////////////////////////////////// 

54 

55 @staticmethod 

56 def validate_file_exists(path: str | Path) -> bool: 

57 """ 

58 Validate that a file exists and is accessible. 

59 

60 Args: 

61 path: Path to the file to validate 

62 

63 Returns: 

64 bool: True if file exists and is accessible, False otherwise 

65 """ 

66 try: 

67 file_path = Path(path) 

68 return file_path.exists() and file_path.is_file() 

69 except Exception: 

70 return False 

71 

72 @staticmethod 

73 def validate_directory_exists(path: str | Path) -> bool: 

74 """ 

75 Validate that a directory exists and is accessible. 

76 

77 Args: 

78 path: Path to the directory to validate 

79 

80 Returns: 

81 bool: True if directory exists and is accessible, False otherwise 

82 """ 

83 try: 

84 dir_path = Path(path) 

85 return dir_path.exists() and dir_path.is_dir() 

86 except Exception: 

87 return False 

88 

89 # //////////////////////////////////////////////// 

90 # DIRECTORY CREATION METHODS 

91 # //////////////////////////////////////////////// 

92 

93 @staticmethod 

94 def create_directory_if_not_exists(path: str | Path) -> None: 

95 """ 

96 Create a directory if it doesn't exist. 

97 

98 Args: 

99 path: Path to the directory to create 

100 

101 Raises: 

102 DirectoryCreationError: If directory creation fails 

103 """ 

104 try: 

105 dir_path = Path(path) 

106 dir_path.mkdir(parents=True, exist_ok=True) 

107 except Exception as e: 

108 raise DirectoryCreationError( 

109 f"Failed to create directory {path}: {e}" 

110 ) from e 

111 

112 @staticmethod 

113 def ensure_parent_directory_exists(file_path: str | Path) -> None: 

114 """ 

115 Ensure that the parent directory of a file exists. 

116 

117 Args: 

118 file_path: Path to the file 

119 

120 Raises: 

121 DirectoryCreationError: If parent directory creation fails 

122 """ 

123 try: 

124 path = Path(file_path) 

125 # Not root directory check 

126 if path.parent != path: 126 ↛ exitline 126 didn't return from function 'ensure_parent_directory_exists' because the condition on line 126 was always true

127 path.parent.mkdir(parents=True, exist_ok=True) 

128 except Exception as e: 

129 raise DirectoryCreationError( 

130 f"Failed to create parent directory for {file_path}: {e}" 

131 ) from e 

132 

133 # //////////////////////////////////////////////// 

134 # FILE SIZE METHODS 

135 # //////////////////////////////////////////////// 

136 

137 @staticmethod 

138 def get_file_size(path: str | Path) -> int: 

139 """ 

140 Get the size of a file in bytes. 

141 

142 Args: 

143 path: Path to the file 

144 

145 Returns: 

146 int: File size in bytes 

147 

148 Raises: 

149 FileNotFoundError: If file doesn't exist 

150 FileAccessError: If file is not accessible 

151 """ 

152 try: 

153 file_path = Path(path) 

154 if not file_path.exists(): 

155 raise FileNotFoundError(f"File does not exist: {path}") 

156 return file_path.stat().st_size 

157 except FileNotFoundError: 

158 raise 

159 except Exception as e: 

160 raise FileAccessError(f"Failed to get file size for {path}: {e}") from e 

161 

162 # //////////////////////////////////////////////// 

163 # FILE OPERATIONS METHODS 

164 # //////////////////////////////////////////////// 

165 

166 @staticmethod 

167 def copy_file( 

168 source: str | Path, 

169 destination: str | Path, 

170 preserve_metadata: bool = True, 

171 ) -> None: 

172 """ 

173 Copy a file from source to destination. 

174 

175 Args: 

176 source: Source file path 

177 destination: Destination file path 

178 preserve_metadata: Whether to preserve file metadata (default: True) 

179 

180 Raises: 

181 FileNotFoundError: If source file doesn't exist 

182 FileCopyError: If copy operation fails 

183 """ 

184 try: 

185 source_path = Path(source) 

186 dest_path = Path(destination) 

187 

188 if not source_path.exists(): 

189 raise FileNotFoundError(f"Source file does not exist: {source}") 

190 

191 # Ensure destination directory exists 

192 FileUtils.ensure_parent_directory_exists(dest_path) 

193 

194 # Copy the file with or without metadata preservation 

195 if preserve_metadata: 

196 shutil.copy2(source_path, dest_path) 

197 else: 

198 shutil.copy(source_path, dest_path) 

199 

200 except FileNotFoundError: 

201 raise 

202 except Exception as e: 

203 raise FileCopyError( 

204 f"Failed to copy file from {source} to {destination}: {e}" 

205 ) from e 

206 

207 @staticmethod 

208 def move_file(source: str | Path, destination: str | Path) -> None: 

209 """ 

210 Move a file from source to destination. 

211 

212 Args: 

213 source: Source file path 

214 destination: Destination file path 

215 

216 Raises: 

217 FileNotFoundError: If source file doesn't exist 

218 FileMoveError: If move operation fails 

219 """ 

220 try: 

221 source_path = Path(source) 

222 dest_path = Path(destination) 

223 

224 if not source_path.exists(): 

225 raise FileNotFoundError(f"Source file does not exist: {source}") 

226 

227 # Ensure destination directory exists 

228 FileUtils.ensure_parent_directory_exists(dest_path) 

229 

230 # Move the file 

231 shutil.move(str(source_path), str(dest_path)) 

232 

233 except FileNotFoundError: 

234 raise 

235 except Exception as e: 

236 raise FileMoveError( 

237 f"Failed to move file from {source} to {destination}: {e}" 

238 ) from e 

239 

240 @staticmethod 

241 def delete_file(path: str | Path) -> None: 

242 """ 

243 Delete a file. 

244 

245 Args: 

246 path: Path to the file to delete 

247 

248 Raises: 

249 FileDeleteError: If deletion fails 

250 """ 

251 try: 

252 file_path = Path(path) 

253 if file_path.exists(): 

254 file_path.unlink() 

255 except Exception as e: 

256 raise FileDeleteError(f"Failed to delete file {path}: {e}") from e 

257 

258 # //////////////////////////////////////////////// 

259 # LISTING AND EXTENSION METHODS 

260 # //////////////////////////////////////////////// 

261 

262 @staticmethod 

263 def list_files(directory: str | Path, pattern: str = "*") -> list[Path]: 

264 """ 

265 List all files in a directory matching a pattern. 

266 

267 Args: 

268 directory: Directory to search in 

269 pattern: File pattern to match (default: "*") 

270 

271 Returns: 

272 list[Path]: List of matching file paths 

273 

274 Raises: 

275 FileNotFoundError: If directory doesn't exist 

276 DirectoryListError: If directory access fails 

277 """ 

278 try: 

279 dir_path = Path(directory) 

280 if not dir_path.exists(): 

281 raise FileNotFoundError(f"Directory does not exist: {directory}") 

282 

283 return list(dir_path.glob(pattern)) 

284 except FileNotFoundError: 

285 raise 

286 except Exception as e: 

287 raise DirectoryListError(f"Failed to list files in {directory}: {e}") from e 

288 

289 @staticmethod 

290 def get_file_extension(path: str | Path) -> str: 

291 """ 

292 Get the file extension. 

293 

294 Args: 

295 path: Path to the file 

296 

297 Returns: 

298 str: File extension (including the dot) 

299 """ 

300 return Path(path).suffix 

301 

302 @staticmethod 

303 def get_file_name_without_extension(path: str | Path) -> str: 

304 """ 

305 Get the file name without extension. 

306 

307 Args: 

308 path: Path to the file 

309 

310 Returns: 

311 str: File name without extension 

312 """ 

313 return Path(path).stem 

314 

315 # //////////////////////////////////////////////// 

316 # PATH UTILITIES METHODS 

317 # //////////////////////////////////////////////// 

318 

319 @staticmethod 

320 def is_hidden_file(path: str | Path) -> bool: 

321 """ 

322 Check if a file is hidden. 

323 

324 Args: 

325 path: Path to the file 

326 

327 Returns: 

328 bool: True if file is hidden, False otherwise 

329 """ 

330 file_path = Path(path) 

331 return file_path.name.startswith(".") 

332 

333 @staticmethod 

334 def get_relative_path(base_path: str | Path, target_path: str | Path) -> str: 

335 """ 

336 Get the relative path from base_path to target_path. 

337 

338 Args: 

339 base_path: Base directory path 

340 target_path: Target file/directory path 

341 

342 Returns: 

343 str: Relative path from base to target 

344 

345 Note: 

346 If target is not relative to base, returns the absolute path. 

347 """ 

348 try: 

349 base = Path(base_path).resolve() 

350 target = Path(target_path).resolve() 

351 return str(target.relative_to(base)) 

352 except ValueError: 

353 # If target is not relative to base, return the absolute path 

354 return str(target) 

355 

356 @staticmethod 

357 def normalize_path(path: str | Path) -> Path: 

358 """ 

359 Normalize a path (resolve symlinks, make absolute). 

360 

361 Args: 

362 path: Path to normalize 

363 

364 Returns: 

365 Path: Normalized absolute path 

366 """ 

367 return Path(path).resolve() 

368 

369 @staticmethod 

370 def ensure_unique_filename(path: str | Path) -> Path: 

371 """ 

372 Ensure a filename is unique by adding a number suffix if necessary. 

373 

374 Args: 

375 path: Original file path 

376 

377 Returns: 

378 Path: Unique file path, with numeric suffix added if original exists 

379 

380 Example: 

381 >>> # If "output.txt" exists, returns "output_1.txt" 

382 >>> unique_path = FileUtils.ensure_unique_filename("output.txt") 

383 """ 

384 file_path = Path(path) 

385 if not file_path.exists(): 

386 return file_path 

387 

388 counter = 1 

389 while True: 

390 new_path = file_path.with_name( 

391 f"{file_path.stem}_{counter}{file_path.suffix}" 

392 ) 

393 if not new_path.exists(): 

394 return new_path 

395 counter += 1