Skip to content

Commit 83dbd19

Browse files
stonebigclaude
andcommitted
say it once, and say when
The -v header read "with every entry / another one already pulls in commented out", a sentence broken where it made no sense. It now carries the source and a timestamp, then the very counts stderr prints -- one wording, from top_level_summary(), for both. Under -v stderr stays quiet rather than repeating them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 94e99af commit 83dbd19

3 files changed

Lines changed: 40 additions & 19 deletions

File tree

README_PYPI.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,15 @@ Those two lines are commented although they go to stderr, so folding both stream
112112
one file (`> new.txt 2>&1`) still leaves a file pip can read.
113113

114114
The notes the source file carried are kept, since they are its author's. `-v` adds the
115-
reasoning: where the list came from, and every dropped entry commented out with what
116-
pulls it in, so re-asking for one is uncommenting it.
115+
reasoning: what the list is made from and when, the same counts, and every dropped entry
116+
commented out with what pulls it in, so re-asking for one is uncommenting it. stderr then
117+
keeps quiet, the counts being in the file already.
117118

118119
```console
119120
$ wppm requirements_slim.txt -tl -v -t D:\WPy64\python
120-
# requirements_slim.txt, sorted, with every entry
121-
# another one already pulls in commented out: 160 entries -> 112.
121+
# requirements_slim.txt, sorted, 2026-08-13 19:22:43
122+
# 160 entries -> 112 kept, 48 already pulled in
123+
# 8 repeated, collapsed: brotli, openai, pympler, pytest, python-barcode, ...
122124

123125
...
124126
#numpy # <- baresql, clarabel, cvxpy, dask[array,dataframe,diagnostics], datashader, ...

tests/test_top_level.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
graph, not the packages.
66
"""
77
import json
8+
import re
89
import subprocess
910
import sys
1011
from pathlib import Path
@@ -142,9 +143,16 @@ def test_verbose_comments_out_what_went_and_why(self, pip):
142143
assert "app" in lines
143144
assert "#lib # <- app" in lines
144145

145-
def test_verbose_heads_the_list_with_its_counts(self, pip):
146-
lines = wppm_module.top_level_as_requirements(pip.top_level(["app", "lib", "orphan"]), verbose=True)
147-
assert "3 entries -> 2" in "\n".join(lines[:2])
146+
def test_verbose_heads_the_list_with_source_and_time(self, pip):
147+
lines = wppm_module.top_level_as_requirements(
148+
pip.top_level(["app", "lib"]), source="req.txt", verbose=True)
149+
assert re.fullmatch(r"# req\.txt, sorted, \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}", lines[0])
150+
151+
def test_verbose_heads_the_list_with_the_same_counts_stderr_gives(self, pip):
152+
result = pip.top_level(["app", "lib", "orphan"])
153+
lines = wppm_module.top_level_as_requirements(result, verbose=True)
154+
assert lines[1:2] == wppm_module.top_level_summary(result)[:1]
155+
assert lines[1] == "# 3 entries -> 2 kept, 1 already pulled in"
148156

149157
def test_source_notes_are_kept_even_plainly(self, pip):
150158
"""They are the author's own lines, not our commentary."""
@@ -202,6 +210,12 @@ def test_the_short_flag_does_the_same(self, graph):
202210
def test_verbose_adds_the_reasoning(self, graph):
203211
assert "#lib # <- app" in self.wppm("-t", str(graph), "--top-level", "-v").splitlines()
204212

213+
def test_verbose_does_not_say_the_counts_twice(self, graph):
214+
"""Under -v they head the list, so stderr keeps quiet."""
215+
proc = self.run("-t", str(graph), "--top-level", "-v")
216+
assert "entries ->" in proc.stdout
217+
assert "entries ->" not in proc.stderr
218+
205219
def test_top_level_of_a_requirements_file(self, graph, tmp_path):
206220
req = tmp_path / "req.txt"
207221
req.write_text("# keep me\nlib\napp\n", encoding="utf-8")

wppm/wppm.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import shutil
1313
import subprocess
1414
import json
15+
from datetime import datetime
1516
from pathlib import Path
1617
from argparse import ArgumentParser, RawTextHelpFormatter
1718
from . import utils, piptree, diff, __version__
@@ -286,17 +287,18 @@ def top_level_as_requirements(result, comments=(), source=None, verbose=False):
286287
287288
Plainly, it is that file and nothing else: the entries, plus the notes the
288289
source itself carried, so redirecting the output replaces the source
289-
without losing what its author wrote in it. -v adds the reasoning -- where
290-
the list came from, and every dropped entry commented out with what pulls
291-
it in, so re-asking for one is uncommenting it.
290+
without losing what its author wrote in it. -v adds the reasoning -- what
291+
the list is made from and when, the same counts stderr gives, and every
292+
dropped entry commented out with what pulls it in, so re-asking for one is
293+
uncommenting it.
292294
"""
293-
kept, dropped = result["kept"], result["dropped"]
294295
lines = []
295296
if verbose:
296-
lines += [f"# {Path(source).name if source else 'installed packages'}, sorted, with every entry",
297-
f"# another one already pulls in commented out: {len(kept) + len(dropped)} entries -> {len(kept)}.",
298-
""]
299-
lines += kept
297+
lines += [f"# {Path(source).name if source else 'installed packages'}, sorted,"
298+
f" {datetime.now():%Y-%m-%d %H:%M:%S}"]
299+
lines += top_level_summary(result) + [""]
300+
lines += result["kept"]
301+
dropped = result["dropped"]
300302
if verbose and dropped:
301303
lines += ["", "# ---- already pulled in by an entry above ----"]
302304
for text, pullers in dropped.items():
@@ -308,8 +310,10 @@ def top_level_as_requirements(result, comments=(), source=None, verbose=False):
308310
def top_level_summary(result):
309311
"""What the caller should know about the answer, rather than of it.
310312
311-
Commented, though it goes to stderr: someone will fold the two streams
312-
into one file sooner or later, and a comment costs nothing.
313+
Goes to stderr, so a redirected list stays a list -- and heads the list
314+
itself under -v, where the reasoning belongs in the file. Commented either
315+
way: someone will fold the two streams into one file sooner or later, and a
316+
comment costs nothing.
313317
"""
314318
kept, dropped = result["kept"], result["dropped"]
315319
notes = [f"{len(kept) + len(dropped)} entries -> {len(kept)} kept, {len(dropped)} already pulled in"]
@@ -384,8 +388,9 @@ def main(test=False):
384388
sys.exit()
385389
for line in top_level_as_requirements(result, comments, source, args.verbose):
386390
print(line)
387-
for note in top_level_summary(result): # stderr: a redirected list stays a list
388-
print(note, file=sys.stderr)
391+
if not args.verbose: # -v already heads the list with them; don't say it twice
392+
for note in top_level_summary(result):
393+
print(note, file=sys.stderr)
389394
sys.exit()
390395
elif args.list:
391396
pip = piptree.PipData(targetpython, args.wheelsource)

0 commit comments

Comments
 (0)