"""Path utilities for the Auto README Generator.""" import os import re from pathlib import Path from typing import Optional class PathUtils: """Utility class for path operations.""" IGNORED_DIRS = { "__pycache__", ".git", ".svn", ".hg", "__MACOSX", ".DS_Store", "node_modules", ".venv", "venv", "ENV", "env", ".tox", ".nox", "build", "dist", "target", ".idea", ".vscode", } IGNORED_FILES = { ".DS_Store", ".gitignore", ".gitattributes", ".gitmodules", } SOURCE_EXTENSIONS = { ".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".scala", } CONFIG_EXTENSIONS = {".json", ".yaml", ".yml", ".toml", ".cfg", ".ini", ".conf"} DOCUMENTATION_EXTENSIONS = {".md", ".rst", ".txt", ".adoc"} TEST_PATTERNS = [ re.compile(r"^test_"), re.compile(r"_test\.py$"), re.compile(r"^tests?\/"), re.compile(r"spec\."), re.compile(r"\.test\."), re.compile(r"\.tests\."), ] @classmethod def normalize_path(cls, path: str | Path) -> Path: """Normalize a path to absolute form.""" return Path(path).resolve() @classmethod def is_ignored_dir(cls, path: Path) -> bool: """Check if a directory should be ignored.""" return path.name in cls.IGNORED_DIRS or path.name.startswith(".") @classmethod def is_ignored_file(cls, path: Path) -> bool: """Check if a file should be ignored.""" return path.name in cls.IGNORED_FILES @classmethod def is_source_file(cls, path: Path) -> bool: """Check if a file is a source code file.""" return path.suffix in cls.SOURCE_EXTENSIONS @classmethod def is_config_file(cls, path: Path) -> bool: """Check if a file is a configuration file.""" return path.suffix in cls.CONFIG_EXTENSIONS @classmethod def is_documentation_file(cls, path: Path) -> bool: """Check if a file is a documentation file.""" return path.suffix in cls.DOCUMENTATION_EXTENSIONS @classmethod def is_test_file(cls, path: Path) -> bool: """Check if a file is a test file.""" for pattern in cls.TEST_PATTERNS: if pattern.search(str(path)): return True return False @classmethod def get_relative_path(cls, path: Path, base: Path) -> Path: """Get relative path from base directory.""" try: return path.relative_to(base) except ValueError: return path @classmethod def is_hidden(cls, path: Path) -> bool: """Check if a path is hidden (starts with dot).""" return path.name.startswith(".") @classmethod def detect_project_root(cls, path: Path) -> Optional[Path]: """Detect project root by looking for marker files.""" markers = [ "pyproject.toml", "package.json", "go.mod", "Cargo.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pom.xml", "build.gradle", ] current = cls.normalize_path(path) for _ in range(50): for marker in markers: if (current / marker).exists(): return current if current.parent == current: break current = current.parent return None @classmethod def is_file_empty(cls, path: Path) -> bool: """Check if a file is empty.""" try: return os.path.getsize(path) == 0 except OSError: return True @classmethod def get_file_size(cls, path: Path) -> int: """Get file size in bytes.""" try: return os.path.getsize(path) except OSError: return 0 @classmethod def count_lines(cls, path: Path) -> int: """Count lines in a file.""" try: with open(path, "r", encoding="utf-8", errors="ignore") as f: return sum(1 for _ in f) except OSError: return 0 @classmethod def safe_read(cls, path: Path, max_size: int = 1024 * 1024) -> Optional[str]: """Safely read file contents with size limit.""" try: if cls.get_file_size(path) > max_size: return None with open(path, "r", encoding="utf-8", errors="ignore") as f: return f.read() except OSError: return None def normalize_path(path: str | Path) -> Path: """Normalize a path to absolute form.""" return PathUtils.normalize_path(path)