Skip to content

Commit e1479d7

Browse files
authored
Merge pull request #843 from iangelak/753-uv-prefix-debug-output
fix: Add package prefix to uv output
2 parents 3a9bfec + 792432c commit e1479d7

File tree

3 files changed

+106
-15
lines changed

3 files changed

+106
-15
lines changed

src/fromager/external_commands.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import typing
88
from io import TextIOWrapper
99

10+
from . import log
11+
1012
logger = logging.getLogger(__name__)
1113

1214
HERE = pathlib.Path(__file__).absolute().parent
@@ -98,8 +100,33 @@ def run(
98100
stdin=stdin,
99101
)
100102
output = completed.stdout.decode("utf-8") if completed.stdout else ""
103+
104+
# Add package prefix to continuation lines for greppability
105+
prefix = log.get_log_prefix()
106+
formatted_output = None
107+
108+
if output:
109+
if prefix:
110+
# FromagerLogRecord handles first line, we handle continuation lines
111+
formatted_output = output.rstrip("\n").replace("\n", f"\n{prefix}: ")
112+
else:
113+
formatted_output = output
114+
101115
if completed.returncode != 0:
102-
logger.error("%s failed with %s", cmd, output)
116+
# Prefix first line for error output (embedded in larger message)
117+
if formatted_output and prefix:
118+
output_to_log = f"\n{prefix}: {formatted_output}"
119+
elif formatted_output:
120+
output_to_log = f"\n{formatted_output}"
121+
else:
122+
output_to_log = ""
123+
logger.error(
124+
"command failed with exit code %d: %s%s",
125+
completed.returncode,
126+
shlex.join(cmd),
127+
output_to_log,
128+
)
129+
103130
err_type = subprocess.CalledProcessError
104131
if network_isolation:
105132
# Look for a few common messages that mean there is a network
@@ -113,5 +140,9 @@ def run(
113140
if substr in output:
114141
err_type = NetworkIsolationError
115142
raise err_type(completed.returncode, cmd, output)
116-
logger.debug("output: %s", output)
143+
144+
# Log command output for debugging
145+
if formatted_output:
146+
logger.debug(formatted_output)
147+
117148
return output

src/fromager/log.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,27 @@
1717
version_ctxvar: contextvars.ContextVar[Version] = contextvars.ContextVar("version")
1818

1919

20+
def get_log_prefix() -> str | None:
21+
"""Get the log prefix based on current context (requirement and version).
22+
23+
Returns:
24+
- "name-version" if both requirement and version are available
25+
- "name" if only requirement is available
26+
- None if no context is available
27+
"""
28+
try:
29+
req = requirement_ctxvar.get()
30+
except LookupError:
31+
return None
32+
else:
33+
try:
34+
version = version_ctxvar.get()
35+
except LookupError:
36+
return req.name
37+
else:
38+
return f"{req.name}-{version}"
39+
40+
2041
@contextlib.contextmanager
2142
def req_ctxvar_context(
2243
req: Requirement, version: Version | None = None
@@ -53,17 +74,10 @@ class FromagerLogRecord(logging.LogRecord):
5374

5475
def getMessage(self) -> str: # noqa: N802
5576
msg = super().getMessage()
56-
try:
57-
req = requirement_ctxvar.get()
58-
except LookupError:
59-
return msg
60-
else:
61-
try:
62-
version = version_ctxvar.get()
63-
except LookupError:
64-
return f"{req.name}: {msg}"
65-
else:
66-
return f"{req.name}-{version}: {msg}"
77+
prefix = get_log_prefix()
78+
if prefix:
79+
return f"{prefix}: {msg}"
80+
return msg
6781

6882

6983
class ThreadLogFilter(logging.Filter):

tests/test_external_commands.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import logging
12
import os
23
import pathlib
34
import subprocess
45
import typing
56
from unittest import mock
67

78
import pytest
9+
from packaging.requirements import Requirement
10+
from packaging.version import Version
811

9-
from fromager import external_commands
12+
from fromager import external_commands, log
1013

1114

1215
def test_external_commands_environ() -> None:
@@ -29,7 +32,10 @@ def test_external_commands_log_file(tmp_path: pathlib.Path) -> None:
2932
assert "test\n" == file_contents
3033

3134

32-
@mock.patch("subprocess.run", return_value=mock.Mock(returncode=0))
35+
@mock.patch(
36+
"subprocess.run",
37+
return_value=mock.Mock(returncode=0, stdout=b"test output\n"),
38+
)
3339
@mock.patch(
3440
"fromager.external_commands.network_isolation_cmd",
3541
return_value=["/bin/unshare", "--net", "--map-current-user"],
@@ -85,3 +91,43 @@ def test_external_commands_network_isolation_real() -> None:
8591
)
8692
exc = typing.cast(subprocess.CalledProcessError, e.value)
8793
assert exc.returncode == 1
94+
95+
96+
def test_external_command_output_prefix(caplog: pytest.LogCaptureFixture) -> None:
97+
"""Test that external command output is prefixed with package name on each line."""
98+
# Set up the log record factory to enable automatic prefixing
99+
old_factory = logging.getLogRecordFactory()
100+
logging.setLogRecordFactory(log.FromagerLogRecord)
101+
102+
try:
103+
req = Requirement("test-package==1.0.0")
104+
version = Version("1.0.0")
105+
106+
with log.req_ctxvar_context(req, version):
107+
with caplog.at_level(logging.DEBUG, logger="fromager.external_commands"):
108+
# Run a command that produces multi-line output
109+
# Use printf for cross-platform compatibility (echo -e doesn't work on macOS)
110+
external_commands.run(["printf", "line1\\nline2\\nline3"])
111+
112+
# Get the last debug log record (the output message)
113+
output_rec = caplog.records[-1]
114+
message = output_rec.getMessage()
115+
116+
# Verify that each line has the package name prefix
117+
# The first line gets the prefix from FromagerLogRecord.getMessage()
118+
# Continuation lines get it from external_commands.run()
119+
expected_prefix = "test-package-1.0.0: "
120+
assert message.startswith(expected_prefix), (
121+
f"Message should start with '{expected_prefix}'"
122+
)
123+
124+
# Check that all lines have the prefix
125+
lines = message.split("\n")
126+
for line in lines:
127+
if line: # Skip empty lines
128+
assert line.startswith(expected_prefix), (
129+
f"Line '{line}' should start with '{expected_prefix}'"
130+
)
131+
finally:
132+
# Restore the original log record factory
133+
logging.setLogRecordFactory(old_factory)

0 commit comments

Comments
 (0)