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
« 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# ///////////////////////////////////////////////////////////////
6"""
7File utilities - File and directory operation utilities for EzCompiler.
9This module provides utility functions for common file operations including
10path validation, directory creation, file copying, and path manipulation.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18# Standard library imports
19import shutil
20from pathlib import Path
22# Local imports
23from ..shared.exceptions.utils import (
24 DirectoryCreationError,
25 DirectoryListError,
26 FileAccessError,
27 FileCopyError,
28 FileDeleteError,
29 FileMoveError,
30 FileNotFoundError,
31)
33# ///////////////////////////////////////////////////////////////
34# CLASSES
35# ///////////////////////////////////////////////////////////////
38class FileUtils:
39 """
40 File and directory operations utility class.
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.
46 Example:
47 >>> FileUtils.create_directory_if_not_exists("./output")
48 >>> file_size = FileUtils.get_file_size("path/to/file.txt")
49 """
51 # ////////////////////////////////////////////////
52 # VALIDATION METHODS
53 # ////////////////////////////////////////////////
55 @staticmethod
56 def validate_file_exists(path: str | Path) -> bool:
57 """
58 Validate that a file exists and is accessible.
60 Args:
61 path: Path to the file to validate
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
72 @staticmethod
73 def validate_directory_exists(path: str | Path) -> bool:
74 """
75 Validate that a directory exists and is accessible.
77 Args:
78 path: Path to the directory to validate
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
89 # ////////////////////////////////////////////////
90 # DIRECTORY CREATION METHODS
91 # ////////////////////////////////////////////////
93 @staticmethod
94 def create_directory_if_not_exists(path: str | Path) -> None:
95 """
96 Create a directory if it doesn't exist.
98 Args:
99 path: Path to the directory to create
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
112 @staticmethod
113 def ensure_parent_directory_exists(file_path: str | Path) -> None:
114 """
115 Ensure that the parent directory of a file exists.
117 Args:
118 file_path: Path to the file
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
133 # ////////////////////////////////////////////////
134 # FILE SIZE METHODS
135 # ////////////////////////////////////////////////
137 @staticmethod
138 def get_file_size(path: str | Path) -> int:
139 """
140 Get the size of a file in bytes.
142 Args:
143 path: Path to the file
145 Returns:
146 int: File size in bytes
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
162 # ////////////////////////////////////////////////
163 # FILE OPERATIONS METHODS
164 # ////////////////////////////////////////////////
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.
175 Args:
176 source: Source file path
177 destination: Destination file path
178 preserve_metadata: Whether to preserve file metadata (default: True)
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)
188 if not source_path.exists():
189 raise FileNotFoundError(f"Source file does not exist: {source}")
191 # Ensure destination directory exists
192 FileUtils.ensure_parent_directory_exists(dest_path)
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)
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
207 @staticmethod
208 def move_file(source: str | Path, destination: str | Path) -> None:
209 """
210 Move a file from source to destination.
212 Args:
213 source: Source file path
214 destination: Destination file path
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)
224 if not source_path.exists():
225 raise FileNotFoundError(f"Source file does not exist: {source}")
227 # Ensure destination directory exists
228 FileUtils.ensure_parent_directory_exists(dest_path)
230 # Move the file
231 shutil.move(str(source_path), str(dest_path))
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
240 @staticmethod
241 def delete_file(path: str | Path) -> None:
242 """
243 Delete a file.
245 Args:
246 path: Path to the file to delete
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
258 # ////////////////////////////////////////////////
259 # LISTING AND EXTENSION METHODS
260 # ////////////////////////////////////////////////
262 @staticmethod
263 def list_files(directory: str | Path, pattern: str = "*") -> list[Path]:
264 """
265 List all files in a directory matching a pattern.
267 Args:
268 directory: Directory to search in
269 pattern: File pattern to match (default: "*")
271 Returns:
272 list[Path]: List of matching file paths
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}")
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
289 @staticmethod
290 def get_file_extension(path: str | Path) -> str:
291 """
292 Get the file extension.
294 Args:
295 path: Path to the file
297 Returns:
298 str: File extension (including the dot)
299 """
300 return Path(path).suffix
302 @staticmethod
303 def get_file_name_without_extension(path: str | Path) -> str:
304 """
305 Get the file name without extension.
307 Args:
308 path: Path to the file
310 Returns:
311 str: File name without extension
312 """
313 return Path(path).stem
315 # ////////////////////////////////////////////////
316 # PATH UTILITIES METHODS
317 # ////////////////////////////////////////////////
319 @staticmethod
320 def is_hidden_file(path: str | Path) -> bool:
321 """
322 Check if a file is hidden.
324 Args:
325 path: Path to the file
327 Returns:
328 bool: True if file is hidden, False otherwise
329 """
330 file_path = Path(path)
331 return file_path.name.startswith(".")
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.
338 Args:
339 base_path: Base directory path
340 target_path: Target file/directory path
342 Returns:
343 str: Relative path from base to target
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)
356 @staticmethod
357 def normalize_path(path: str | Path) -> Path:
358 """
359 Normalize a path (resolve symlinks, make absolute).
361 Args:
362 path: Path to normalize
364 Returns:
365 Path: Normalized absolute path
366 """
367 return Path(path).resolve()
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.
374 Args:
375 path: Original file path
377 Returns:
378 Path: Unique file path, with numeric suffix added if original exists
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
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