Skip to content

Commit e87bfe2

Browse files
committed
rimport: Add/improve docstrings.
1 parent 3609962 commit e87bfe2

1 file changed

Lines changed: 98 additions & 8 deletions

File tree

rimport

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ STAGE_OWNER = "cesmdata"
2424

2525

2626
def build_parser() -> argparse.ArgumentParser:
27+
"""Build and configure the argument parser for rimport.
28+
29+
Creates an ArgumentParser with the following options:
30+
- Mutually exclusive required group:
31+
--file: Import a single file (relative to inputdata directory)
32+
--list: Import multiple files from a list file
33+
- Optional:
34+
--inputdata: Override the default inputdata directory
35+
36+
Returns:
37+
argparse.ArgumentParser: Configured parser ready to parse command-line arguments.
38+
"""
2739
parser = argparse.ArgumentParser(
2840
description="Copy files from CESM inputdata directory to a publishing directory."
2941
)
@@ -64,7 +76,18 @@ def build_parser() -> argparse.ArgumentParser:
6476

6577

6678
def read_filelist(list_path: Path) -> List[str]:
67-
"""Read list file, ignoring blank lines and comments starting with #."""
79+
"""Read a file list and return non-empty, non-comment lines.
80+
81+
Reads a text file containing a list of filenames, filtering out:
82+
- Blank lines (empty or whitespace-only)
83+
- Comment lines (starting with '#')
84+
85+
Args:
86+
list_path: Path to the file containing the list of filenames.
87+
88+
Returns:
89+
List of stripped, non-empty lines that are not comments.
90+
"""
6891
lines: List[str] = []
6992
with list_path.open("r", encoding="utf-8") as f:
7093
for raw in f:
@@ -76,6 +99,20 @@ def read_filelist(list_path: Path) -> List[str]:
7699

77100

78101
def resolve_paths(root: Path, relnames: Iterable[str]) -> List[Path]:
102+
"""Convert relative or absolute path names to resolved absolute Paths.
103+
104+
For each name in relnames:
105+
- If the name is relative, it is resolved relative to `root`
106+
- If the name is already absolute, it is resolved as-is
107+
All paths are resolved to their canonical absolute form.
108+
109+
Args:
110+
root: Base directory for resolving relative paths.
111+
relnames: Iterable of path names (relative or absolute) to resolve.
112+
113+
Returns:
114+
List of resolved absolute Path objects.
115+
"""
79116
paths: List[Path] = []
80117
for name in relnames:
81118
p = (
@@ -89,13 +126,25 @@ def resolve_paths(root: Path, relnames: Iterable[str]) -> List[Path]:
89126

90127
def stage_data(src: Path, inputdata_root: Path, staging_root: Path) -> None:
91128
"""Stage a file by mirroring its path under `staging_root`.
92-
Destination path is computed by replacing the `args.inputdata` prefix of `src`
129+
130+
Destination path is computed by replacing the `inputdata_root` prefix of `src`
93131
with `staging_root`, i.e.:
94-
dst = staging_root / src.relative_to(inputdata_root)
132+
dst = staging_root / src.relative_to(inputdata_root)
133+
134+
Args:
135+
src: Source file path to stage.
136+
inputdata_root: Root directory of the inputdata tree.
137+
staging_root: Root directory where files will be staged.
138+
139+
Raises:
140+
RuntimeError: If `src` is a live symlink (already published), or if `src`
141+
is outside the inputdata root, or if `src` is already under staging directory.
142+
RuntimeError: If `src` is a broken symlink.
143+
FileNotFoundError: If `src` does not exist.
95144
96145
Guardrails:
97-
* Raise if `src` is a *live* symlink ("already published").
98-
* Raise if `src` is a broken symlink or is outside the inputdata root.
146+
* Raise if `src` is a *live* symlink ("already published").
147+
* Raise if `src` is a broken symlink or is outside the inputdata root.
99148
"""
100149
if src.is_symlink() and src.exists():
101150
# TODO: This should be a regular message, not an error.
@@ -123,7 +172,22 @@ def stage_data(src: Path, inputdata_root: Path, staging_root: Path) -> None:
123172

124173

125174
def ensure_running_as(target_user: str, argv: list[str]) -> None:
126-
"""If not running as `target_user`, re-exec via sudo -u target_user (handles 2FA via PAM)."""
175+
"""Ensure the script is running as the target user, re-executing via sudo if needed.
176+
177+
If not running as `target_user`, re-exec via sudo -u target_user (handles 2FA via PAM).
178+
This function will not return if re-execution is needed; it replaces the current process.
179+
180+
Args:
181+
target_user: Username to run as (e.g., 'cesmdata').
182+
argv: Command-line arguments to pass to the re-executed process.
183+
184+
Raises:
185+
SystemExit: If the target user is not found on the system (exit code 2).
186+
SystemExit: If not running interactively and authentication is required (exit code 2).
187+
188+
Note:
189+
If re-execution is needed, this function calls os.execvp() and does not return.
190+
"""
127191
try:
128192
target_uid = pwd.getpwnam(target_user).pw_uid
129193
except KeyError as exc:
@@ -148,15 +212,41 @@ def ensure_running_as(target_user: str, argv: list[str]) -> None:
148212

149213

150214
def get_staging_root() -> Path:
151-
"""Return the staging root. Uses $RIMPORT_STAGING if set, otherwise
152-
creates a sibling directory named '<inputdata_root>.staging'."""
215+
"""Return the staging root directory path.
216+
217+
Uses $RIMPORT_STAGING if set, otherwise returns the default staging root.
218+
219+
Returns:
220+
Path: Resolved absolute path to the staging root directory.
221+
"""
153222
env = os.getenv("RIMPORT_STAGING")
154223
if env:
155224
return Path(env).expanduser().resolve()
156225
return DEFAULT_STAGING_ROOT
157226

158227

159228
def main(argv: List[str] | None = None) -> int:
229+
"""Main entry point for the rimport tool.
230+
231+
Copies files from the CESM inputdata directory to a staging/publishing directory,
232+
preserving the directory structure. Ensures the script runs as the correct user
233+
(STAGE_OWNER) and handles both single files and file lists.
234+
235+
Args:
236+
argv: Command-line arguments to parse. If None, uses sys.argv.
237+
238+
Returns:
239+
int: Exit code (0 for success, 1 if any files had errors, 2 for fatal errors).
240+
241+
Environment Variables:
242+
RIMPORT_SKIP_USER_CHECK: Set to "1" to skip automatic user switching.
243+
RIMPORT_STAGING: Override the default staging root directory.
244+
245+
Exit Codes:
246+
0: All files staged successfully.
247+
1: One or more files failed to stage (errors printed to stderr).
248+
2: Fatal error (missing inputdata directory, missing file list, etc.).
249+
"""
160250
parser = build_parser()
161251
args = parser.parse_args(argv)
162252

0 commit comments

Comments
 (0)