Skip to content

Commit 64b41f6

Browse files
committed
Port FilePaths and Platforms from appose-java
1 parent 3abbbed commit 64b41f6

4 files changed

Lines changed: 372 additions & 58 deletions

File tree

src/appose/environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
import os
3737
from pathlib import Path
3838

39-
from .paths import find_exe
39+
from .filepath import find_exe
4040
from .service import Service
4141

4242

src/appose/filepath.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Copyright (C) 2023 - 2025 Appose developers.
2+
# SPDX-License-Identifier: BSD-2-Clause
3+
4+
"""
5+
Utility functions for working with files and paths.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import re
11+
import shutil
12+
from pathlib import Path
13+
14+
from . import platform
15+
16+
17+
def location(c: type) -> Path | None:
18+
"""
19+
Gets the path to the module file containing the given class/type.
20+
21+
Args:
22+
c: The class/type whose file path should be discerned.
23+
24+
Returns:
25+
Path to the file containing the given class, or None if not found.
26+
"""
27+
try:
28+
if hasattr(c, "__file__") and c.__file__:
29+
return Path(c.__file__)
30+
# Try to get module
31+
if hasattr(c, "__module__"):
32+
import sys
33+
34+
module = sys.modules.get(c.__module__)
35+
if module and hasattr(module, "__file__") and module.__file__:
36+
return Path(module.__file__)
37+
except Exception:
38+
pass
39+
return None
40+
41+
42+
def file_type(name: str) -> str:
43+
"""
44+
Extracts the file extension(s) from a filename.
45+
46+
Args:
47+
name: The filename to extract extension from.
48+
49+
Returns:
50+
The extension including the dot (e.g., ".txt", ".tar.gz"), or empty string if none.
51+
"""
52+
pattern = re.compile(r".*?((\.[a-zA-Z0-9]+)+)$")
53+
match = pattern.match(name)
54+
if not match:
55+
return ""
56+
return match.group(1)
57+
58+
59+
def find_exe(dirs: list[str], exes: list[str]) -> Path | None:
60+
"""
61+
Finds an executable file by searching for it in a list of directories.
62+
63+
Args:
64+
dirs: List of directory paths to search.
65+
exes: List of executable names/paths to search for.
66+
67+
Returns:
68+
Path to the first matching executable found, or None if not found.
69+
"""
70+
for exe in exes:
71+
exe_path = Path(exe)
72+
if exe_path.is_absolute():
73+
# Candidate is an absolute path; check it directly.
74+
if platform.is_executable(exe_path):
75+
return exe_path
76+
else:
77+
# Candidate is a relative path; check beneath each given directory.
78+
for dir_str in dirs:
79+
candidate = Path(dir_str) / exe
80+
if platform.is_executable(candidate) and not candidate.is_dir():
81+
return candidate
82+
return None
83+
84+
85+
def move_directory(src_dir: Path, dest_dir: Path, overwrite: bool) -> None:
86+
"""
87+
Merges the files of the given source directory into the specified destination directory.
88+
89+
For example, move_directory(foo, bar) would move:
90+
- foo/a.txt → bar/a.txt
91+
- foo/b.dat → bar/b.dat
92+
- foo/subfoo/d.doc → bar/subfoo/d.doc
93+
94+
Args:
95+
src_dir: Source directory whose contents will be moved.
96+
dest_dir: Destination directory into which the source directory's contents will be merged.
97+
overwrite: If True, overwrite existing destination files; if False, back up source files instead.
98+
99+
Raises:
100+
IOError: If the operation fails.
101+
"""
102+
ensure_directory(src_dir)
103+
ensure_directory(dest_dir)
104+
105+
for item in src_dir.iterdir():
106+
move_file(item, dest_dir, overwrite)
107+
108+
# Remove the now-empty source directory
109+
src_dir.rmdir()
110+
111+
112+
def move_file(src_file: Path, dest_dir: Path, overwrite: bool) -> None:
113+
"""
114+
Moves the given source file to the destination directory,
115+
creating intermediate destination directories as needed.
116+
117+
If the destination file already exists, one of two things will happen:
118+
A) The existing destination file will be renamed as a backup to file.ext.old
119+
(or file.ext.0.old, file.ext.1.old, etc., if file.ext.old already exists), or
120+
B) The source file will be renamed as a backup in this manner.
121+
122+
Which behavior occurs depends on the value of the overwrite flag:
123+
True to back up the destination file, or False to back up the source file.
124+
125+
Args:
126+
src_file: Source file to move.
127+
dest_dir: Destination directory into which the file will be moved.
128+
overwrite: If True, "overwrite" the destination file with the source file,
129+
backing up any existing destination file first; if False,
130+
leave the original destination file in place, instead moving
131+
the source file to a backup destination as a "previous" version.
132+
133+
Raises:
134+
IOError: If something goes wrong with the needed I/O operations.
135+
"""
136+
dest_file = dest_dir / src_file.name
137+
138+
if src_file.is_dir():
139+
# Create matching destination directory as needed.
140+
dest_file.mkdir(parents=True, exist_ok=True)
141+
# Recurse over source directory contents.
142+
move_directory(src_file, dest_file, overwrite)
143+
return
144+
145+
# Source file is not a directory; move it into the destination directory.
146+
if dest_dir.exists() and not dest_dir.is_dir():
147+
raise ValueError(f"Non-directory destination path: {dest_dir}")
148+
149+
dest_dir.mkdir(parents=True, exist_ok=True)
150+
151+
if dest_file.exists() and not overwrite:
152+
# Destination already exists, and we aren't allowed to rename it.
153+
# So we instead rename the source file directly to a backup filename
154+
# in the destination directory.
155+
rename_to_backup(src_file, dest_dir)
156+
return
157+
158+
# Rename the existing destination file (if any) to a backup file,
159+
# then move the source file into place.
160+
rename_to_backup(dest_file)
161+
src_file.rename(dest_file)
162+
163+
164+
def rename_to_backup(src_file: Path, dest_dir: Path | None = None) -> None:
165+
"""
166+
Renames the given file to a backup filename.
167+
168+
The file will be renamed to filename.ext.old, or filename.ext.0.old,
169+
filename.ext.1.old, etc., if filename.ext.old already exists.
170+
171+
Args:
172+
src_file: Source file to rename to a backup.
173+
dest_dir: Destination directory where the backup file will be created.
174+
If None, uses the source file's parent directory.
175+
176+
Raises:
177+
IOError: If something goes wrong with the needed I/O operations.
178+
"""
179+
if not src_file.exists():
180+
return # Nothing to back up!
181+
182+
if dest_dir is None:
183+
dest_dir = src_file.parent
184+
185+
prefix = src_file.name
186+
suffix = "old"
187+
backup_file = dest_dir / f"{prefix}.{suffix}"
188+
189+
# Try to find a non-existing backup filename
190+
for i in range(1000):
191+
if not backup_file.exists():
192+
break
193+
backup_file = dest_dir / f"{prefix}.{i}.{suffix}"
194+
195+
if backup_file.exists():
196+
failed_target = dest_dir / f"{prefix}.{suffix}"
197+
raise OSError(
198+
f"Too many backup files already exist for target: {failed_target}"
199+
)
200+
201+
src_file.rename(backup_file)
202+
203+
204+
def delete_recursively(dir: Path) -> None:
205+
"""
206+
Deletes a directory and all its contents recursively.
207+
Properly handles symlinks including broken ones.
208+
209+
Args:
210+
dir: The directory to delete.
211+
212+
Raises:
213+
IOError: If deletion fails.
214+
"""
215+
if not dir.exists() and not dir.is_symlink():
216+
return
217+
218+
if dir.is_dir() and not dir.is_symlink():
219+
shutil.rmtree(dir)
220+
else:
221+
dir.unlink()
222+
223+
224+
def ensure_directory(file: Path) -> None:
225+
"""
226+
Checks that the given path is an existing directory, raising an exception if not.
227+
228+
Args:
229+
file: The path to check.
230+
231+
Raises:
232+
IOError: If the given path does not exist, or is not a directory.
233+
"""
234+
if not file.exists():
235+
raise IOError(f"Directory does not exist: {file}")
236+
if not file.is_file():
237+
raise IOError(f"Not a directory: {file}")

src/appose/paths.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)