Skip to content

Commit 9691530

Browse files
committed
relink.py: Use Python logging instead of print() statements.
1 parent cb1b51d commit 9691530

2 files changed

Lines changed: 94 additions & 53 deletions

File tree

relink.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
import os
88
import pwd
99
import argparse
10+
import logging
1011

1112
DEFAULT_SOURCE_ROOT = '/glade/campaign/cesm/cesmdata/cseg/inputdata/'
1213
DEFAULT_TARGET_ROOT = '/glade/campaign/collections/gdex/data/d651077/cesmdata/inputdata/'
1314

15+
# Set up logger
16+
logger = logging.getLogger(__name__)
17+
1418
def find_and_replace_owned_files(source_dir, target_dir, username):
1519
"""
1620
Finds files owned by a specific user in a source directory tree,
@@ -29,10 +33,15 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
2933
try:
3034
user_uid = pwd.getpwnam(username).pw_uid
3135
except KeyError:
32-
print(f"Error: User '{username}' not found. Exiting.")
36+
logger.error("Error: User '%s' not found. Exiting.", username)
3337
return
3438

35-
print(f"Searching for files owned by '{username}' (UID: {user_uid}) in '{source_dir}'...")
39+
logger.info(
40+
"Searching for files owned by '%s' (UID: %s) in '%s'...",
41+
username,
42+
user_uid,
43+
source_dir
44+
)
3645

3746
for dirpath, _, filenames in os.walk(source_dir):
3847
for filename in filenames:
@@ -41,23 +50,28 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
4150
# Use os.stat().st_uid to get the file's owner UID
4251
try:
4352
if os.path.islink(file_path):
44-
print(f"Skipping symlink: {file_path}")
53+
logger.info("Skipping symlink: %s", file_path)
4554
continue
4655

4756
file_uid = os.stat(file_path).st_uid
4857
except FileNotFoundError:
4958
continue # Skip if file was deleted during traversal
5059

5160
if file_uid == user_uid:
52-
print(f"Found owned file: {file_path}")
61+
logger.info("Found owned file: %s", file_path)
5362

5463
# Determine the relative path and the new link's destination
5564
relative_path = os.path.relpath(file_path, source_dir)
5665
link_target = os.path.join(target_dir, relative_path)
5766

5867
# Check if the target file actually exists
5968
if not os.path.exists(link_target):
60-
print(f"Warning: Corresponding file not found in '{target_dir}' for '{file_path}'. Skipping.")
69+
logger.warning(
70+
"Warning: Corresponding file not found in '%s' "
71+
"for '%s'. Skipping.",
72+
target_dir,
73+
file_path
74+
)
6175
continue
6276

6377
# Get the link name
@@ -66,9 +80,9 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
6680
# Remove the original file
6781
try:
6882
os.rename(link_name, link_name+".tmp")
69-
print(f"Deleted original file: {link_name}")
83+
logger.info("Deleted original file: %s", link_name)
7084
except OSError as e:
71-
print(f"Error deleting file {link_name}: {e}. Skipping.")
85+
logger.error("Error deleting file %s: %s. Skipping.", link_name, e)
7286
continue
7387

7488
# Create the symbolic link, handling necessary parent directories
@@ -77,10 +91,10 @@ def find_and_replace_owned_files(source_dir, target_dir, username):
7791
os.makedirs(os.path.dirname(link_name), exist_ok=True)
7892
os.symlink(link_target, link_name)
7993
os.remove(link_name+".tmp")
80-
print(f"Created symbolic link: {link_name} -> {link_target}")
94+
logger.info("Created symbolic link: %s -> %s", link_name, link_target)
8195
except OSError as e:
8296
os.rename(link_name+".tmp", link_name)
83-
print(f"Error creating symlink for {link_name}: {e}. Skipping.")
97+
logger.error("Error creating symlink for %s: %s. Skipping.", link_name, e)
8498

8599
def parse_arguments():
86100
"""
@@ -114,6 +128,14 @@ def parse_arguments():
114128
return parser.parse_args()
115129

116130
if __name__ == '__main__':
131+
# Configure logging to display INFO and above to console (stdout)
132+
import sys
133+
logging.basicConfig(
134+
level=logging.INFO,
135+
format='%(message)s',
136+
stream=sys.stdout
137+
)
138+
117139
# --- Configuration ---
118140
args = parse_arguments()
119141
my_username = os.environ['USER']

tests/test_relink.py

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,29 @@
77
import tempfile
88
import shutil
99
import pwd
10+
import logging
1011
from unittest.mock import patch
12+
1113
import pytest
1214

1315
# Add parent directory to path to import relink module
1416
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15-
import relink
17+
import relink # noqa: E402
18+
19+
20+
@pytest.fixture(scope="function", autouse=True)
21+
def configure_logging():
22+
"""Configure logging to output to stdout for all tests."""
23+
# Configure logging before each test
24+
logging.basicConfig(
25+
level=logging.INFO,
26+
format="%(message)s",
27+
stream=sys.stdout,
28+
force=True, # Force reconfiguration
29+
)
30+
yield
31+
# Clean up logging handlers after each test
32+
logging.getLogger().handlers.clear()
1633

1734

1835
class TestFindAndReplaceOwnedFiles:
@@ -86,7 +103,7 @@ def test_nested_directory_structure(self, temp_dirs, current_user):
86103
assert os.path.islink(source_file), "Nested file should be a symlink"
87104
assert os.readlink(source_file) == target_file
88105

89-
def test_skip_existing_symlinks(self, temp_dirs, current_user, capsys):
106+
def test_skip_existing_symlinks(self, temp_dirs, current_user, caplog):
90107
"""Test that existing symlinks are skipped."""
91108
source_dir, target_dir = temp_dirs
92109
username = current_user
@@ -107,7 +124,8 @@ def test_skip_existing_symlinks(self, temp_dirs, current_user, capsys):
107124
mtime_before = stat_before.st_mtime
108125

109126
# Run the function
110-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
127+
with caplog.at_level(logging.INFO):
128+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
111129

112130
# Verify the symlink is unchanged (same inode means it wasn't deleted/recreated)
113131
stat_after = os.lstat(source_link)
@@ -119,12 +137,11 @@ def test_skip_existing_symlinks(self, temp_dirs, current_user, capsys):
119137
os.readlink(source_link) == dummy_target
120138
), "Symlink target should be unchanged"
121139

122-
# Check that "Skipping symlink" message was printed
123-
captured = capsys.readouterr()
124-
assert "Skipping symlink:" in captured.out
125-
assert source_link in captured.out
140+
# Check that "Skipping symlink" message was logged
141+
assert "Skipping symlink:" in caplog.text
142+
assert source_link in caplog.text
126143

127-
def test_missing_target_file(self, temp_dirs, current_user, capsys):
144+
def test_missing_target_file(self, temp_dirs, current_user, caplog):
128145
"""Test behavior when target file doesn't exist."""
129146
source_dir, target_dir = temp_dirs
130147
username = current_user
@@ -135,17 +152,17 @@ def test_missing_target_file(self, temp_dirs, current_user, capsys):
135152
f.write("orphan content")
136153

137154
# Run the function
138-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
155+
with caplog.at_level(logging.INFO):
156+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
139157

140158
# Verify the file is NOT converted to symlink
141159
assert not os.path.islink(source_file), "File should not be a symlink"
142160
assert os.path.isfile(source_file), "Original file should still exist"
143161

144162
# Check warning message
145-
captured = capsys.readouterr()
146-
assert "Warning: Corresponding file not found" in captured.out
163+
assert "Warning: Corresponding file not found" in caplog.text
147164

148-
def test_invalid_username(self, temp_dirs, capsys):
165+
def test_invalid_username(self, temp_dirs, caplog):
149166
"""Test behavior with invalid username."""
150167
source_dir, target_dir = temp_dirs
151168

@@ -159,12 +176,14 @@ def test_invalid_username(self, temp_dirs, capsys):
159176
raise RuntimeError(f"{invalid_username=} DOES actually exist")
160177

161178
# Run the function
162-
relink.find_and_replace_owned_files(source_dir, target_dir, invalid_username)
179+
with caplog.at_level(logging.INFO):
180+
relink.find_and_replace_owned_files(
181+
source_dir, target_dir, invalid_username
182+
)
163183

164184
# Check error message
165-
captured = capsys.readouterr()
166-
assert "Error: User" in captured.out
167-
assert "not found" in captured.out
185+
assert "Error: User" in caplog.text
186+
assert "not found" in caplog.text
168187

169188
def test_multiple_files(self, temp_dirs, current_user):
170189
"""Test with multiple files in the directory."""
@@ -220,20 +239,20 @@ def test_absolute_paths(self, temp_dirs, current_user):
220239
finally:
221240
os.chdir(cwd)
222241

223-
def test_print_searching_message(self, temp_dirs, current_user, capsys):
242+
def test_print_searching_message(self, temp_dirs, current_user, caplog):
224243
"""Test that searching message is printed."""
225244
source_dir, target_dir = temp_dirs
226245
username = current_user
227246

228247
# Run the function
229-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
248+
with caplog.at_level(logging.INFO):
249+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
230250

231-
# Check that searching message was printed
232-
captured = capsys.readouterr()
233-
assert f"Searching for files owned by '{username}'" in captured.out
234-
assert f"in '{os.path.abspath(source_dir)}'" in captured.out
251+
# Check that searching message was logged
252+
assert f"Searching for files owned by '{username}'" in caplog.text
253+
assert f"in '{os.path.abspath(source_dir)}'" in caplog.text
235254

236-
def test_print_found_owned_file(self, temp_dirs, current_user, capsys):
255+
def test_print_found_owned_file(self, temp_dirs, current_user, caplog):
237256
"""Test that 'Found owned file' message is printed."""
238257
source_dir, target_dir = temp_dirs
239258
username = current_user
@@ -248,14 +267,14 @@ def test_print_found_owned_file(self, temp_dirs, current_user, capsys):
248267
f.write("target content")
249268

250269
# Run the function
251-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
270+
with caplog.at_level(logging.INFO):
271+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
252272

253-
# Check that "Found owned file" message was printed
254-
captured = capsys.readouterr()
255-
assert "Found owned file:" in captured.out
256-
assert source_file in captured.out
273+
# Check that "Found owned file" message was logged
274+
assert "Found owned file:" in caplog.text
275+
assert source_file in caplog.text
257276

258-
def test_print_deleted_and_created_messages(self, temp_dirs, current_user, capsys):
277+
def test_print_deleted_and_created_messages(self, temp_dirs, current_user, caplog):
259278
"""Test that deleted and created symlink messages are printed."""
260279
source_dir, target_dir = temp_dirs
261280
username = current_user
@@ -270,13 +289,13 @@ def test_print_deleted_and_created_messages(self, temp_dirs, current_user, capsy
270289
f.write("target")
271290

272291
# Run the function
273-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
292+
with caplog.at_level(logging.INFO):
293+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
274294

275295
# Check messages
276-
captured = capsys.readouterr()
277-
assert "Deleted original file:" in captured.out
278-
assert "Created symbolic link:" in captured.out
279-
assert f"{source_file} -> {target_file}" in captured.out
296+
assert "Deleted original file:" in caplog.text
297+
assert "Created symbolic link:" in caplog.text
298+
assert f"{source_file} -> {target_file}" in caplog.text
280299

281300

282301
class TestParseArguments:
@@ -387,7 +406,7 @@ def test_file_with_special_characters(self, temp_dirs):
387406
assert os.path.islink(source_file)
388407
assert os.readlink(source_file) == target_file
389408

390-
def test_error_deleting_file(self, temp_dirs, capsys):
409+
def test_error_deleting_file(self, temp_dirs, caplog):
391410
"""Test error message when file deletion fails."""
392411
source_dir, target_dir = temp_dirs
393412
username = os.environ["USER"]
@@ -407,14 +426,14 @@ def mock_rename(src, dst):
407426

408427
with patch("os.rename", side_effect=mock_rename):
409428
# Run the function
410-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
429+
with caplog.at_level(logging.INFO):
430+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
411431

412432
# Check error message
413-
captured = capsys.readouterr()
414-
assert "Error deleting file" in captured.out
415-
assert source_file in captured.out
433+
assert "Error deleting file" in caplog.text
434+
assert source_file in caplog.text
416435

417-
def test_error_creating_symlink(self, temp_dirs, capsys):
436+
def test_error_creating_symlink(self, temp_dirs, caplog):
418437
"""Test error message when symlink creation fails."""
419438
source_dir, target_dir = temp_dirs
420439
username = os.environ["USER"]
@@ -434,9 +453,9 @@ def mock_symlink(src, dst):
434453

435454
with patch("os.symlink", side_effect=mock_symlink):
436455
# Run the function
437-
relink.find_and_replace_owned_files(source_dir, target_dir, username)
456+
with caplog.at_level(logging.INFO):
457+
relink.find_and_replace_owned_files(source_dir, target_dir, username)
438458

439459
# Check error message
440-
captured = capsys.readouterr()
441-
assert "Error creating symlink" in captured.out
442-
assert source_file in captured.out
460+
assert "Error creating symlink" in caplog.text
461+
assert source_file in caplog.text

0 commit comments

Comments
 (0)