From f2d988ce9760e9bafaa784d1be3cbcc6a2cc11ff Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 08:45:40 +0000 Subject: [PATCH] Add utils, templates, config, interactive, and github modules --- src/auto_readme/utils/path_utils.py | 190 ++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/auto_readme/utils/path_utils.py diff --git a/src/auto_readme/utils/path_utils.py b/src/auto_readme/utils/path_utils.py new file mode 100644 index 0000000..246a77c --- /dev/null +++ b/src/auto_readme/utils/path_utils.py @@ -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)