191 lines
4.9 KiB
Python
191 lines
4.9 KiB
Python
"""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)
|