Skip to content

Commit eae695e

Browse files
kurtmckeenedbat
andauthored
feat: add a print subcommand (#140)
* Add a `print` subcommand Closes #115 * Apply copy edit suggestions Co-authored-by: Ned Batchelder <[email protected]> * Address a failing test * Update docs/commands.rst * Update changelog.d/20250311_074712_kurtmckee_print_command_issue_115.rst * Expand `print` command testing to 100% coverage --------- Co-authored-by: Ned Batchelder <[email protected]>
1 parent ec6c7b6 commit eae695e

File tree

6 files changed

+207
-2
lines changed

6 files changed

+207
-2
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Added
2+
.....
3+
4+
- Add a ``print`` command that can write changelog entries to standard out
5+
or to a file, closing `issue 115`_. Thanks, `Kurt McKee <pull 140_>`_
6+
7+
.. _issue 115: https://github.com/nedbat/scriv/issues/115
8+
.. _pull 140: https://github.com/nedbat/scriv/pull/140
9+

docs/commands.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,4 +230,31 @@ If your changelog file is in reStructuredText format, you will need `pandoc`_
230230

231231
.. _pandoc: https://pandoc.org/
232232

233+
scriv print
234+
===========
235+
236+
.. [[[cog show_help("print") ]]]
237+
238+
.. code::
239+
240+
$ scriv print --help
241+
Usage: scriv print [OPTIONS]
242+
243+
Print collected fragments, or print an entry from the changelog.
244+
245+
Options:
246+
--version TEXT The version of the changelog entry to extract.
247+
--output PATH The path to a file to write the output to.
248+
-v, --verbosity LVL Either CRITICAL, ERROR, WARNING, INFO or DEBUG
249+
--help Show this message and exit.
250+
.. [[[end]]] (checksum: f652a3470da5f726b13ba076471b2444)
251+
252+
The ``print`` command writes a changelog entry to standard out.
253+
254+
If ``--output`` is provided, the changelog entry is written to the given file.
255+
256+
If ``--version`` is given, the requested changelog entry is extracted
257+
from the CHANGELOG.
258+
If not, then the changelog entry is generated from uncollected fragment files.
259+
233260
.. include:: include/links.rst

src/scriv/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .collect import collect
1010
from .create import create
1111
from .ghrel import github_release
12+
from .print import print_
1213

1314
click_log.basic_config(logging.getLogger())
1415

@@ -28,3 +29,4 @@ def cli() -> None: # noqa: D401
2829
cli.add_command(create)
2930
cli.add_command(collect)
3031
cli.add_command(github_release)
32+
cli.add_command(print_)

src/scriv/print.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Collecting fragments."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
import os
7+
import pathlib
8+
import sys
9+
10+
import click
11+
12+
from .scriv import Scriv
13+
from .util import Version, scriv_command
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
@click.command(name="print")
19+
@click.option(
20+
"--version",
21+
default=None,
22+
help="The version of the changelog entry to extract.",
23+
)
24+
@click.option(
25+
"--output",
26+
type=click.Path(),
27+
default=None,
28+
help="The path to a file to write the output to.",
29+
)
30+
@scriv_command
31+
def print_(
32+
version: str | None,
33+
output: pathlib.Path | None,
34+
) -> None:
35+
"""
36+
Print collected fragments, or print an entry from the changelog.
37+
"""
38+
scriv = Scriv()
39+
changelog = scriv.changelog()
40+
newline = os.linesep
41+
42+
if version is None:
43+
logger.info(f"Generating entry from {scriv.config.fragment_directory}")
44+
frags = scriv.fragments_to_combine()
45+
if not frags:
46+
logger.info("No changelog fragments to collect")
47+
sys.exit(2)
48+
contents = changelog.entry_text(scriv.combine_fragments(frags)).strip()
49+
else:
50+
logger.info(f"Extracting entry for {version} from {changelog.path}")
51+
changelog.read()
52+
newline = changelog.newline
53+
target_version = Version(version)
54+
for etitle, sections in changelog.entries().items():
55+
eversion = Version.from_text(str(etitle))
56+
if eversion is None:
57+
continue
58+
if eversion == target_version:
59+
contents = f"{changelog.newline * 2}".join(sections).strip()
60+
break
61+
else:
62+
logger.info(f"Unable to find version {version} in the changelog")
63+
sys.exit(2)
64+
65+
if output:
66+
# Standardize newlines to match either the platform default
67+
# or to match the existing newlines found in the CHANGELOG.
68+
contents_raw = newline.join(contents.splitlines()).encode("utf-8")
69+
with open(output, "wb") as file:
70+
file.write(contents_raw)
71+
else:
72+
# Standardize newlines to just '\n' when writing to STDOUT.
73+
contents = "\n".join(contents.splitlines())
74+
print(contents)

tests/conftest.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ def cli_invoke(temp_dir: Path):
6161
"""
6262

6363
def invoke(command, expect_ok=True):
64-
runner = CliRunner()
64+
runner = CliRunner(mix_stderr=False)
6565
result = runner.invoke(scriv_cli, command)
66-
print(result.output)
66+
print(result.stdout, end="")
67+
print(result.stderr, end="", file=sys.stderr)
6768
if result.exception:
6869
traceback.print_exception(
6970
None, result.exception, result.exception.__traceback__

tests/test_print.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""Test print logic."""
2+
3+
import freezegun
4+
import pytest
5+
6+
CHANGELOG_HEADER = """\
7+
8+
1.2 - 2020-02-25
9+
================
10+
"""
11+
12+
13+
FRAG = """\
14+
Fixed
15+
-----
16+
17+
- Launching missiles no longer targets ourselves.
18+
"""
19+
20+
21+
@pytest.mark.parametrize("newline", ("\r\n", "\n"))
22+
def test_print_fragment(newline, cli_invoke, changelog_d, temp_dir, capsys):
23+
fragment = FRAG.replace("\n", newline).encode("utf-8")
24+
(changelog_d / "20170616_nedbat.rst").write_bytes(fragment)
25+
26+
with freezegun.freeze_time("2020-02-25T15:18:19"):
27+
cli_invoke(["print"])
28+
29+
std = capsys.readouterr()
30+
assert std.out == FRAG
31+
32+
33+
@pytest.mark.parametrize("newline", ("\r\n", "\n"))
34+
def test_print_fragment_output(
35+
newline, cli_invoke, changelog_d, temp_dir, capsys
36+
):
37+
fragment = FRAG.replace("\n", newline).encode("utf-8")
38+
(changelog_d / "20170616_nedbat.rst").write_bytes(fragment)
39+
output_file = temp_dir / "output.txt"
40+
41+
with freezegun.freeze_time("2020-02-25T15:18:19"):
42+
cli_invoke(["print", "--output", output_file])
43+
44+
std = capsys.readouterr()
45+
assert std.out == ""
46+
assert output_file.read_text().strip() == FRAG.strip()
47+
48+
49+
@pytest.mark.parametrize("newline", ("\r\n", "\n"))
50+
def test_print_changelog(newline, cli_invoke, changelog_d, temp_dir, capsys):
51+
changelog = (CHANGELOG_HEADER + FRAG).replace("\n", newline).encode("utf-8")
52+
(temp_dir / "CHANGELOG.rst").write_bytes(changelog)
53+
54+
with freezegun.freeze_time("2020-02-25T15:18:19"):
55+
cli_invoke(["print", "--version", "1.2"])
56+
57+
std = capsys.readouterr()
58+
assert std.out == FRAG
59+
60+
61+
@pytest.mark.parametrize("newline", ("\r\n", "\n"))
62+
def test_print_changelog_output(
63+
newline, cli_invoke, changelog_d, temp_dir, capsys
64+
):
65+
changelog = (CHANGELOG_HEADER + FRAG).replace("\n", newline).encode("utf-8")
66+
(temp_dir / "CHANGELOG.rst").write_bytes(changelog)
67+
output_file = temp_dir / "output.txt"
68+
69+
with freezegun.freeze_time("2020-02-25T15:18:19"):
70+
cli_invoke(["print", "--version", "1.2", "--output", output_file])
71+
72+
std = capsys.readouterr()
73+
assert std.out == ""
74+
assert output_file.read_bytes().decode() == FRAG.strip().replace(
75+
"\n", newline
76+
)
77+
78+
79+
def test_print_no_fragments(cli_invoke):
80+
result = cli_invoke(["print"], expect_ok=False)
81+
82+
assert result.exit_code == 2
83+
assert "No changelog fragments to collect" in result.stderr
84+
85+
86+
def test_print_version_not_in_changelog(cli_invoke, changelog_d, temp_dir):
87+
(temp_dir / "CHANGELOG.rst").write_bytes(b"BOGUS\n=====\n\n1.0\n===")
88+
89+
result = cli_invoke(["print", "--version", "123.456"], expect_ok=False)
90+
91+
assert result.exit_code == 2
92+
assert "Unable to find version 123.456 in the changelog" in result.stderr

0 commit comments

Comments
 (0)