Add utils, templates, config, interactive, and github modules
This commit is contained in:
190
src/auto_readme/utils/path_utils.py
Normal file
190
src/auto_readme/utils/path_utils.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user