Refactor shared script helpers
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def existing_input_paths(args: list[str], *, print_missing: bool = False) -> list[Path]:
|
||||
paths: list[Path] = []
|
||||
for arg in args:
|
||||
path = Path(arg).expanduser()
|
||||
if path.exists():
|
||||
paths.append(path.resolve())
|
||||
elif print_missing:
|
||||
print(f"Skipping missing path: {arg}")
|
||||
return paths
|
||||
|
||||
|
||||
def determine_working_dir(args: list[str]) -> Path:
|
||||
paths = existing_input_paths(args)
|
||||
if not paths:
|
||||
raise RuntimeError("No existing input paths were provided.")
|
||||
if len(paths) == 1:
|
||||
path = paths[0]
|
||||
return path if path.is_dir() else path.parent
|
||||
|
||||
try:
|
||||
common = Path(os.path.commonpath([str(path) for path in paths]))
|
||||
except ValueError as exc:
|
||||
raise RuntimeError("Input paths do not have a common ancestor.") from exc
|
||||
|
||||
if str(common) == common.anchor:
|
||||
raise RuntimeError("Input paths only share the filesystem root; choose items closer together.")
|
||||
return common
|
||||
|
||||
|
||||
def collect_files(
|
||||
args: list[str],
|
||||
*,
|
||||
allowed_exts: set[str] | None = None,
|
||||
print_missing: bool = False,
|
||||
sort_by_name: bool = False,
|
||||
) -> list[Path]:
|
||||
files: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
|
||||
for arg in args:
|
||||
path = Path(arg).expanduser()
|
||||
if path.is_file():
|
||||
candidates = [path] if allowed_exts is None or path.suffix.lower() in allowed_exts else []
|
||||
elif path.is_dir():
|
||||
candidates = [
|
||||
child
|
||||
for child in path.rglob("*")
|
||||
if child.is_file()
|
||||
and (allowed_exts is None or child.suffix.lower() in allowed_exts)
|
||||
]
|
||||
else:
|
||||
if print_missing:
|
||||
print(f"Skipping missing path: {arg}")
|
||||
continue
|
||||
|
||||
for candidate in candidates:
|
||||
resolved = candidate.resolve()
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
files.append(resolved)
|
||||
|
||||
if sort_by_name:
|
||||
return sorted(files, key=lambda path: path.name.lower())
|
||||
return sorted(files, key=lambda path: str(path).lower())
|
||||
|
||||
|
||||
def first_file_from_path(path: Path) -> Path | None:
|
||||
path = path.expanduser()
|
||||
if path.is_file():
|
||||
return path.resolve()
|
||||
if path.is_dir():
|
||||
for child in sorted(path.rglob("*"), key=lambda item: str(item).lower()):
|
||||
if child.is_file():
|
||||
return child.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def cleanup_roots_from_args(args: list[str]) -> list[Path]:
|
||||
roots: list[Path] = []
|
||||
seen: set[Path] = set()
|
||||
for arg in args:
|
||||
path = Path(arg).expanduser()
|
||||
if path.is_dir():
|
||||
resolved = path.resolve()
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
roots.append(resolved)
|
||||
return roots
|
||||
|
||||
|
||||
def unique_path(path: Path) -> Path:
|
||||
if not path.exists():
|
||||
return path
|
||||
|
||||
for index in range(1, 10000):
|
||||
candidate = path.with_name(f"{path.stem}-{index}{path.suffix}")
|
||||
if not candidate.exists():
|
||||
return candidate
|
||||
|
||||
raise RuntimeError(f"Could not find an available name for {path}")
|
||||
|
||||
|
||||
def unique_existing_target(path: Path, planned_sources: set[Path]) -> Path:
|
||||
if not path.exists() or path.resolve() in planned_sources:
|
||||
return path
|
||||
for index in range(1, 10000):
|
||||
candidate = path.with_name(f"{path.stem}-{index}{path.suffix}")
|
||||
if not candidate.exists() or candidate.resolve() in planned_sources:
|
||||
return candidate
|
||||
raise RuntimeError(f"Could not find available target name for {path}")
|
||||
|
||||
|
||||
def remove_empty_dirs(cleanup_roots: list[Path], keep_dirs: set[Path]) -> list[Path]:
|
||||
removed: list[Path] = []
|
||||
candidates: set[Path] = set()
|
||||
for root in cleanup_roots:
|
||||
if not root.exists() or not root.is_dir():
|
||||
continue
|
||||
for directory, subdirs, _files in os.walk(root, topdown=False):
|
||||
path = Path(directory).resolve()
|
||||
candidates.add(path)
|
||||
for subdir in subdirs:
|
||||
child = (Path(directory) / subdir).resolve()
|
||||
if child.exists() and child.is_dir():
|
||||
candidates.add(child)
|
||||
|
||||
for directory in sorted(candidates, key=lambda path: len(path.parts), reverse=True):
|
||||
if directory in keep_dirs:
|
||||
continue
|
||||
try:
|
||||
directory.rmdir()
|
||||
removed.append(directory)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
def display_path_from_working_dir(path: Path, working_dir: Path) -> str:
|
||||
try:
|
||||
relative = path.resolve().relative_to(working_dir.resolve())
|
||||
except ValueError:
|
||||
return str(path)
|
||||
return "." if str(relative) == "." else str(relative)
|
||||
Reference in New Issue
Block a user