Skip to content

Commit 7311b72

Browse files
committed
rimport: Add --quiet and --verbose options.
1 parent b013904 commit 7311b72

6 files changed

Lines changed: 158 additions & 14 deletions

File tree

relink.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import time
1313
from pathlib import Path
1414

15-
from shared import DEFAULT_INPUTDATA_ROOT, DEFAULT_STAGING_ROOT
15+
from shared import DEFAULT_INPUTDATA_ROOT, DEFAULT_STAGING_ROOT, get_log_level
1616

1717
# Set up logger
1818
logger = logging.getLogger(__name__)
@@ -397,12 +397,7 @@ def process_args(args):
397397
args (argparse.Namespace): Parsed command-line arguments.
398398
"""
399399
# Configure logging based on verbosity flags
400-
if args.quiet:
401-
args.log_level = logging.WARNING
402-
elif args.verbose:
403-
args.log_level = logging.DEBUG
404-
else:
405-
args.log_level = logging.INFO
400+
args.log_level = get_log_level(quiet=args.quiet, verbose=args.verbose)
406401

407402
# Ensure that items_to_process is a list
408403
if hasattr(args, "items_to_process") and not isinstance(args.items_to_process, list):

rimport

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ def build_parser() -> argparse.ArgumentParser:
8989
help="Check whether file(s) is/are already published.",
9090
)
9191

92+
# Verbosity options (mutually exclusive)
93+
verbosity_group = parser.add_mutually_exclusive_group()
94+
verbosity_group.add_argument(
95+
"-v", "--verbose", action="store_true", help="Enable verbose output (DEBUG level)"
96+
)
97+
verbosity_group.add_argument(
98+
"-q",
99+
"--quiet",
100+
action="store_true",
101+
help="Quiet mode (show only warnings and errors)",
102+
)
103+
92104
# Provide -help to mirror legacy behavior (in addition to -h and --help)
93105
parser.add_argument(
94106
"-h",
@@ -315,20 +327,23 @@ def print_can_file_be_downloaded(file_can_be_downloaded: bool):
315327
logger.info("%sFile is not (yet) available for download.", INDENT)
316328

317329

318-
def configure_logging() -> None:
330+
def configure_logging(log_level: int = logging.INFO) -> None:
319331
"""Configure logging to send INFO/WARNING to stdout and ERROR/CRITICAL to stderr.
320332
321333
Sets up two handlers:
322-
- INFO handler: Sends INFO and WARNING level messages to stdout
334+
- INFO handler: Sends INFO, WARNING, and DEBUG level messages to stdout
323335
- ERROR handler: Sends ERROR and CRITICAL level messages to stderr
324336
325337
Both handlers use simple message-only formatting without timestamps or level names.
338+
339+
Args:
340+
log_level: Minimum logging level (DEBUG, INFO, or WARNING). Default is INFO.
326341
"""
327-
logger.setLevel(logging.INFO)
342+
logger.setLevel(log_level)
328343

329-
# Handler for INFO and WARNING level messages -> stdout
344+
# Handler for INFO, WARNING, and DEBUG level messages -> stdout
330345
info_handler = logging.StreamHandler(sys.stdout)
331-
info_handler.setLevel(logging.INFO)
346+
info_handler.setLevel(logging.DEBUG) # Accept all levels, filter will handle it
332347
info_handler.addFilter(lambda record: record.levelno < logging.ERROR)
333348
info_handler.setFormatter(logging.Formatter("%(message)s"))
334349

@@ -365,11 +380,13 @@ def main(argv: List[str] | None = None) -> int:
365380
1: One or more files failed to stage (errors printed to stderr).
366381
2: Fatal error (missing inputdata directory, missing file list, etc.).
367382
"""
368-
configure_logging()
369-
370383
parser = build_parser()
371384
args = parser.parse_args(argv)
372385

386+
# Configure logging based on verbosity flags
387+
log_level = shared.get_log_level(quiet=args.quiet, verbose=args.verbose)
388+
configure_logging(log_level)
389+
373390
# Ensure we are running as the STAGE_OWNER account before touching the tree
374391
# Set env var RIMPORT_SKIP_USER_CHECK=1 if you prefer to run `sudox -u STAGE_OWNER rimport …`
375392
# explicitly (or for testing).

shared.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,29 @@
22
Things shared between rimport and relink
33
"""
44

5+
import logging
6+
57
DEFAULT_INPUTDATA_ROOT = "/glade/campaign/cesm/cesmdata/cseg/inputdata/"
68
DEFAULT_STAGING_ROOT = (
79
"/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/"
810
)
11+
12+
13+
def get_log_level(quiet: bool = False, verbose: bool = False) -> int:
14+
"""Determine logging level based on quiet and verbose flags.
15+
16+
Args:
17+
quiet: If True, show only warnings and errors (WARNING level).
18+
verbose: If True, show debug messages (DEBUG level).
19+
20+
Returns:
21+
int: Logging level (DEBUG, INFO, or WARNING).
22+
23+
Note:
24+
If both quiet and verbose are True, quiet takes precedence.
25+
"""
26+
if quiet:
27+
return logging.WARNING
28+
if verbose:
29+
return logging.DEBUG
30+
return logging.INFO

tests/rimport/test_build_parser.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,42 @@ def test_list_with_inputdata(self):
126126
args = parser.parse_args(["-list", "files.txt", "-inputdata", "/my/data"])
127127
assert args.filelist == "files.txt"
128128
assert args.inputdata == "/my/data"
129+
130+
def test_quiet_default(self):
131+
"""Test that quiet defaults to False."""
132+
parser = rimport.build_parser()
133+
args = parser.parse_args(["-file", "test.nc"])
134+
assert args.quiet is False
135+
136+
def test_verbose_default(self):
137+
"""Test that verbose defaults to False."""
138+
parser = rimport.build_parser()
139+
args = parser.parse_args(["-file", "test.nc"])
140+
assert args.verbose is False
141+
142+
@pytest.mark.parametrize("quiet_flag", ["-q", "--quiet"])
143+
def test_quiet_arguments_accepted(self, quiet_flag):
144+
"""Test that all quiet argument flags are accepted."""
145+
parser = rimport.build_parser()
146+
args = parser.parse_args(["-file", "test.nc", quiet_flag])
147+
assert args.quiet is True
148+
assert args.verbose is False
149+
150+
@pytest.mark.parametrize("verbose_flag", ["-v", "--verbose"])
151+
def test_verbose_arguments_accepted(self, verbose_flag):
152+
"""Test that all verbose argument flags are accepted."""
153+
parser = rimport.build_parser()
154+
args = parser.parse_args(["-file", "test.nc", verbose_flag])
155+
assert args.verbose is True
156+
assert args.quiet is False
157+
158+
def test_quiet_and_verbose_mutually_exclusive(self, capsys):
159+
"""Test that -q and -v cannot be used together."""
160+
parser = rimport.build_parser()
161+
with pytest.raises(SystemExit):
162+
parser.parse_args(["-file", "test.nc", "-q", "-v"])
163+
164+
# Check that the error message explains the problem
165+
captured = capsys.readouterr()
166+
stderr_lines = captured.err.strip().split("\n")
167+
assert "not allowed with argument" in stderr_lines[-1]

tests/rimport/test_configure_logging.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,39 @@ def test_multiple_calls_dont_duplicate_handlers(self):
123123

124124
rimport.configure_logging()
125125
assert len(rimport.logger.handlers) == 2 # Still 2, not 6
126+
127+
def test_configure_with_debug_level(self, capsys):
128+
"""Test that configure_logging accepts DEBUG level."""
129+
rimport.configure_logging(logging.DEBUG)
130+
131+
# DEBUG messages should now be logged
132+
rimport.logger.debug("Debug message")
133+
134+
captured = capsys.readouterr()
135+
assert "Debug message" in captured.out
136+
assert captured.err == ""
137+
138+
def test_configure_with_warning_level(self, capsys):
139+
"""Test that configure_logging accepts WARNING level."""
140+
rimport.configure_logging(logging.WARNING)
141+
142+
# INFO messages should be suppressed
143+
rimport.logger.info("Info message")
144+
# WARNING messages should be logged
145+
rimport.logger.warning("Warning message")
146+
147+
captured = capsys.readouterr()
148+
assert "Info message" not in captured.out
149+
assert "Warning message" in captured.out
150+
assert captured.err == ""
151+
152+
def test_configure_with_info_level_suppresses_debug(self, capsys):
153+
"""Test that INFO level suppresses DEBUG messages."""
154+
rimport.configure_logging(logging.INFO)
155+
156+
rimport.logger.debug("Debug message")
157+
rimport.logger.info("Info message")
158+
159+
captured = capsys.readouterr()
160+
assert "Debug message" not in captured.out
161+
assert "Info message" in captured.out

tests/test_shared.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Tests for shared.py module functions.
3+
"""
4+
5+
import logging
6+
import shared
7+
8+
9+
class TestGetLogLevel:
10+
"""Test suite for get_log_level() function."""
11+
12+
def test_default_returns_info(self):
13+
"""Test that default (no flags) returns INFO level."""
14+
result = shared.get_log_level()
15+
assert result == logging.INFO
16+
17+
def test_quiet_returns_warning(self):
18+
"""Test that quiet=True returns WARNING level."""
19+
result = shared.get_log_level(quiet=True)
20+
assert result == logging.WARNING
21+
22+
def test_verbose_returns_debug(self):
23+
"""Test that verbose=True returns DEBUG level."""
24+
result = shared.get_log_level(verbose=True)
25+
assert result == logging.DEBUG
26+
27+
def test_quiet_takes_precedence_over_verbose(self):
28+
"""Test that quiet takes precedence when both are True."""
29+
result = shared.get_log_level(quiet=True, verbose=True)
30+
assert result == logging.WARNING
31+
32+
def test_quiet_false_verbose_false(self):
33+
"""Test explicit False values return INFO."""
34+
result = shared.get_log_level(quiet=False, verbose=False)
35+
assert result == logging.INFO

0 commit comments

Comments
 (0)