Skip to content

Commit 347e95b

Browse files
committed
Group logging under a project name header
Fixes #953
1 parent b4e0450 commit 347e95b

33 files changed

Lines changed: 261 additions & 156 deletions

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Release 0.12.0 (unreleased)
1313
* Fix extra newlines in patch for new files (#945)
1414
* Replace colored-logs and Halo with Rich (#960)
1515
* Respect `NO_COLOR <https://no-color.org/>`_ (#960)
16+
* Group logging under a project name header (#953)
1617

1718
Release 0.11.0 (released 2026-01-03)
1819
====================================

dfetch/commands/common.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,23 +44,19 @@ def _make_recommendation(
4444
recommendations (List[ProjectEntry]): List of recommendations
4545
childmanifest_path (str): Path to the source of recommendations
4646
"""
47-
logger.warning(
48-
"\n".join(
49-
[
50-
"",
51-
f'"{project.name}" depends on the following project(s) '
52-
"which are not part of your manifest:",
53-
f"(found in {childmanifest_path})",
54-
]
55-
)
56-
)
57-
5847
recommendation_json = yaml.dump(
5948
[proj.as_yaml() for proj in recommendations],
6049
indent=4,
6150
sort_keys=False,
6251
)
63-
logger.warning("")
64-
for line in recommendation_json.splitlines():
65-
logger.warning(line)
66-
logger.warning("")
52+
logger.print_warning_line(
53+
project.name,
54+
"\n".join(
55+
[
56+
f'"{project.name}" depends on the following project(s) which are not part of your manifest:',
57+
f"(found in {childmanifest_path})",
58+
"",
59+
recommendation_json,
60+
]
61+
),
62+
)

dfetch/commands/environment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None
2323

2424
def __call__(self, _: argparse.Namespace) -> None:
2525
"""Perform listing the environment."""
26-
logger.print_info_line("platform", f"{platform.system()} {platform.release()}")
26+
logger.print_report_line(
27+
"platform", f"{platform.system()} {platform.release()}"
28+
)
2729
for project_type in SUPPORTED_PROJECT_TYPES:
2830
project_type.list_tool_info()

dfetch/commands/validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ def __call__(self, args: argparse.Namespace) -> None:
3535
manifest_path = find_manifest()
3636
parse(manifest_path)
3737
manifest_path = os.path.relpath(manifest_path, os.getcwd())
38-
logger.print_info_line(manifest_path, "valid")
38+
logger.print_report_line(manifest_path, "valid")

dfetch/log.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,39 @@ def configure_root_logger(console: Optional[Console] = None) -> None:
4848
class DLogger(logging.Logger):
4949
"""Logging class extended with specific log items for dfetch."""
5050

51-
def print_info_line(self, name: str, info: str) -> None:
52-
"""Print a line of info."""
51+
_printed_projects: set[str] = set()
52+
53+
def print_report_line(self, name: str, info: str) -> None:
54+
"""Print a line for a report."""
5355
self.info(
5456
f" [bold][bright_green]{name:20s}:[/bright_green][blue] {info}[/blue][/bold]"
5557
)
5658

59+
def print_info_line(self, name: str, info: str) -> None:
60+
"""Print a line of info, only printing the project name once."""
61+
if name not in DLogger._printed_projects:
62+
self.info(f" [bold][bright_green]{name}:[/bright_green][/bold]")
63+
DLogger._printed_projects.add(name)
64+
65+
line = info.replace("\n", "\n ")
66+
self.info(f" [bold blue]> {line}[/bold blue]")
67+
5768
def print_warning_line(self, name: str, info: str) -> None:
5869
"""Print a warning line: green name, yellow value."""
59-
self.warning(
60-
f" [bold][bright_green]{name:20s}:[/bright_green][bright_yellow] {info}[/bright_yellow][/bold]"
61-
)
70+
if name not in DLogger._printed_projects:
71+
self.info(f" [bold][bright_green]{name}:[/bright_green][/bold]")
72+
DLogger._printed_projects.add(name)
73+
74+
line = info.replace("\n", "\n ")
75+
self.info(f" [bold bright_yellow]> {line}[/bold bright_yellow]")
6276

6377
def print_title(self) -> None:
6478
"""Print the DFetch tool title and version."""
6579
self.info(f"[bold blue]Dfetch ({__version__})[/bold blue]")
6680

6781
def print_info_field(self, field_name: str, field: str) -> None:
6882
"""Print a field with corresponding value."""
69-
self.print_info_line(field_name, field if field else "<none>")
83+
self.print_report_line(field_name, field if field else "<none>")
7084

7185
def warning(self, msg: object, *args: Any, **kwargs: Any) -> None:
7286
"""Log warning."""
@@ -101,11 +115,33 @@ def status(
101115
DLogger._printed_projects.add(name)
102116

103117
return Status(
104-
f"[bold bright_green]{message}[/bold bright_green]",
118+
f"[bold bright_blue]> {message}[/bold bright_blue]",
105119
spinner=spinner,
106120
console=rich_console,
107121
)
108122

123+
@classmethod
124+
def reset_projects(cls) -> None:
125+
"""Clear the record of printed project names."""
126+
cls._printed_projects.clear()
127+
128+
129+
class ExtLogFilter(logging.Filter): # pylint: disable=too-few-public-methods
130+
"""Adds indentation to all log messages that pass through this filter."""
131+
132+
def __init__(self, prefix: str = " "):
133+
"""Initialize the ExtLogFilter with a prefix."""
134+
super().__init__()
135+
self.prefix = prefix
136+
137+
def filter(self, record: logging.LogRecord) -> bool:
138+
"""Add indentation to the log record message."""
139+
color = "blue" if record.levelno < logging.WARNING else "yellow"
140+
141+
line = record.msg.replace("\n", "\n ")
142+
record.msg = f"{self.prefix}[{color}]{line}[/{color}]"
143+
return True
144+
109145

110146
def setup_root(name: str, console: Optional[Console] = None) -> DLogger:
111147
"""Create and return the root logger."""
@@ -153,3 +189,4 @@ def configure_external_logger(name: str, level: int = logging.INFO) -> None:
153189
logger.setLevel(level)
154190
logger.propagate = True
155191
logger.handlers.clear()
192+
logger.addFilter(ExtLogFilter())

dfetch/project/subproject.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,15 @@ def _apply_patches(self) -> list[str]:
156156

157157
normalized_patch_path = str(relative_patch_path.as_posix())
158158

159-
apply_patch(normalized_patch_path, root=self.local_path)
160-
self._log_project(f'Applied patch "{normalized_patch_path}"')
159+
self._log_project(f'Applying patch "{normalized_patch_path}"')
160+
result = apply_patch(normalized_patch_path, root=self.local_path)
161+
162+
if result.encoding_warning:
163+
self._log_project(
164+
f'After retrying found that patch-file "{normalized_patch_path}" '
165+
"is not UTF-8 encoded, consider saving it with UTF-8 encoding."
166+
)
167+
161168
applied_patches.append(normalized_patch_path)
162169
return applied_patches
163170

@@ -223,7 +230,7 @@ def _log_project(self, msg: str) -> None:
223230

224231
@staticmethod
225232
def _log_tool(name: str, msg: str) -> None:
226-
logger.print_info_line(name, msg.strip())
233+
logger.print_report_line(name, msg.strip())
227234

228235
@property
229236
def local_path(self) -> str:
@@ -293,9 +300,10 @@ def on_disk_version(self) -> Optional[Version]:
293300
try:
294301
return Metadata.from_file(self.__metadata.path).version
295302
except TypeError:
296-
logger.warning(
303+
logger.print_warning_line(
304+
self.__project.name,
297305
f"{pathlib.Path(self.__metadata.path).relative_to(os.getcwd()).as_posix()}"
298-
" is an invalid metadata file, not checking on disk version!"
306+
" is an invalid metadata file, not checking on disk version!",
299307
)
300308
return None
301309

@@ -311,9 +319,10 @@ def _on_disk_hash(self) -> Optional[str]:
311319
try:
312320
return Metadata.from_file(self.__metadata.path).hash
313321
except TypeError:
314-
logger.warning(
322+
logger.print_warning_line(
323+
self.__project.name,
315324
f"{pathlib.Path(self.__metadata.path).relative_to(os.getcwd()).as_posix()}"
316-
" is an invalid metadata file, not checking local hash!"
325+
" is an invalid metadata file, not checking local hash!",
317326
)
318327
return None
319328

dfetch/vcs/patch.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@
44
import hashlib
55
import stat
66
from collections.abc import Sequence
7+
from dataclasses import dataclass
78
from pathlib import Path
89

910
import patch_ng
1011

11-
from dfetch.log import configure_external_logger, get_logger
12-
13-
logger = get_logger(__name__)
12+
from dfetch.log import configure_external_logger
1413

1514
configure_external_logger("patch_ng")
1615

1716

17+
@dataclass
18+
class PatchResult:
19+
"""Result of applying a patch."""
20+
21+
encoding_warning: bool = False
22+
23+
1824
def _git_mode(path: Path) -> str:
1925
if path.is_symlink():
2026
return "120000"
@@ -61,26 +67,30 @@ def dump_patch(patch_set: patch_ng.PatchSet) -> str:
6167
return "\n".join(patch_lines) + "\n" if patch_lines else ""
6268

6369

64-
def apply_patch(patch_path: str, root: str = ".") -> None:
70+
def apply_patch(
71+
patch_path: str,
72+
root: str = ".",
73+
) -> PatchResult:
6574
"""Apply the specified patch relative to the root."""
6675
patch_set = patch_ng.fromfile(patch_path)
6776

77+
result = PatchResult()
78+
6879
if not patch_set:
6980
with open(patch_path, "rb") as patch_file:
7081
patch_text = patch_ng.decode_text(patch_file.read()).encode("utf-8")
7182
patch_set = patch_ng.fromstring(patch_text)
7283

7384
if patch_set:
74-
logger.warning(
75-
f'After retrying found that patch-file "{patch_path}" '
76-
"is not UTF-8 encoded, consider saving it with UTF-8 encoding."
77-
)
85+
result.encoding_warning = True
7886

7987
if not patch_set:
8088
raise RuntimeError(f'Invalid patch file: "{patch_path}"')
8189
if not patch_set.apply(strip=0, root=root, fuzz=True):
8290
raise RuntimeError(f'Applying patch "{patch_path}" failed')
8391

92+
return result
93+
8494

8595
def create_svn_patch_for_new_file(file_path: str) -> str:
8696
"""Create a svn patch for a new file."""

doc/_ext/sphinxcontrib_asciinema/.dfetch_data.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
# For more info see https://dfetch.rtfd.io/en/latest/getting_started.html
33
dfetch:
44
branch: master
5-
hash: dcd1473e1a3ca613b804e3e51e7ee342
6-
last_fetch: 07/01/2026, 21:38:48
5+
hash: 5b0a3a18e1e83d363f9eb0ac4b3fca17
6+
last_fetch: 26/01/2026, 23:40:59
77
patch:
88
- doc/_ext/patches/001-autoformat-sphinxcontrib.asciinema.patch
99
- doc/_ext/patches/002-fix-options-sphinxcontrib.asciinema.patch

features/check-git-repo.feature

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ Feature: Checking dependencies from a git repository
2727
Then the output shows
2828
"""
2929
Dfetch (0.11.0)
30-
ext/test-repo-rev-only: wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
31-
ext/test-rev-and-branch: wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
30+
ext/test-repo-rev-only:
31+
> wanted (e1fda19a57b873eb8e6ae37780594cbb77b70f1a), available (e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
32+
ext/test-rev-and-branch:
33+
> wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
3234
"""
3335

3436
Scenario: A newer tag is available than in manifest
@@ -51,7 +53,8 @@ Feature: Checking dependencies from a git repository
5153
Then the output shows
5254
"""
5355
Dfetch (0.11.0)
54-
ext/test-repo-tag-v1: wanted (v1), available (v2.0)
56+
ext/test-repo-tag-v1:
57+
> wanted (v1), available (v2.0)
5558
"""
5659

5760
Scenario: Check is done after an update
@@ -80,8 +83,10 @@ Feature: Checking dependencies from a git repository
8083
Then the output shows
8184
"""
8285
Dfetch (0.11.0)
83-
ext/test-repo-rev-only: up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
84-
ext/test-rev-and-branch: wanted & current (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
86+
ext/test-repo-rev-only:
87+
> up-to-date (e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
88+
ext/test-rev-and-branch:
89+
> wanted & current (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
8590
"""
8691

8792
Scenario: Tag is updated in manifest
@@ -112,7 +117,8 @@ Feature: Checking dependencies from a git repository
112117
Then the output shows
113118
"""
114119
Dfetch (0.11.0)
115-
ext/test-repo-tag : wanted (v2.0), current (v1), available (v2.0)
120+
ext/test-repo-tag:
121+
> wanted (v2.0), current (v1), available (v2.0)
116122
"""
117123

118124
Scenario: A local change is reported
@@ -133,8 +139,9 @@ Feature: Checking dependencies from a git repository
133139
Then the output shows
134140
"""
135141
Dfetch (0.11.0)
136-
SomeProject : Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject'
137-
SomeProject : up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0)
142+
SomeProject:
143+
> Local changes were detected, please generate a patch using 'dfetch diff SomeProject' and add it to your manifest using 'patch:'. Alternatively overwrite the local changes with 'dfetch update --force SomeProject'
144+
> up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0)
138145
"""
139146

140147
Scenario: Change to ignored files are not reported
@@ -153,7 +160,8 @@ Feature: Checking dependencies from a git repository
153160
Then the output shows
154161
"""
155162
Dfetch (0.11.0)
156-
SomeProject : up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0)
163+
SomeProject:
164+
> up-to-date (master - 90be799b58b10971691715bdc751fbe5237848a0)
157165
"""
158166

159167
Scenario: A non-existent remote is reported
@@ -201,9 +209,12 @@ Feature: Checking dependencies from a git repository
201209
Then the output shows
202210
"""
203211
Dfetch (0.11.0)
204-
SomeProjectMissingTag: wanted (i-dont-exist), but not available at the upstream.
205-
SomeProjectNonExistentBranch: wanted (i-dont-exist), but not available at the upstream.
206-
SomeProjectNonExistentRevision: wanted (0123112321234123512361236123712381239123), but not available at the upstream.
212+
SomeProjectMissingTag:
213+
> wanted (i-dont-exist), but not available at the upstream.
214+
SomeProjectNonExistentBranch:
215+
> wanted (i-dont-exist), but not available at the upstream.
216+
SomeProjectNonExistentRevision:
217+
> wanted (0123112321234123512361236123712381239123), but not available at the upstream.
207218
"""
208219

209220
Scenario: Credentials required for remote

features/check-specific-projects.feature

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ Feature: Checking specific projects
2828
Then the output shows
2929
"""
3030
Dfetch (0.11.0)
31-
ext/test-rev-and-branch: wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
31+
ext/test-rev-and-branch:
32+
> wanted (main - 8df389d0524863b85f484f15a91c5f2c40aefda1), available (main - e1fda19a57b873eb8e6ae37780594cbb77b70f1a)
3233
"""

0 commit comments

Comments
 (0)