Skip to content

Commit 24f65a0

Browse files
fizykadiroibanhynek
authored
Add --keep option to allow to generate newsfile, but keep newsfragmen… (#453)
* Add --keep option to allow to generate newsfile, but keep newsfragments - closes #129 * Apply suggestions from code review Co-authored-by: Adi Roiban <adiroiban@gmail.com> * Additional post-PR enhancements * Update build --keep comment to be more relevant Co-authored-by: Adi Roiban <adiroiban@gmail.com> * More compact remove_files and use with_isolated_runner for new tests * Refactored _git.remove_files Code making decision to remove prompt or keep news fragments is being kept in new intermediate function _remover.remove_news_fragment_files * Update src/towncrier/newsfragments/129.feature Co-authored-by: Hynek Schlawack <hs@ox.cx> * Make a decision and return it instead of calling _git.remove underneath * Move should_remove_fragment_files into build module Co-authored-by: Adi Roiban <adiroiban@gmail.com> Co-authored-by: Hynek Schlawack <hs@ox.cx>
1 parent 9554985 commit 24f65a0

File tree

5 files changed

+130
-20
lines changed

5 files changed

+130
-20
lines changed

docs/cli.rst

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ Build the combined news file from news fragments.
5050
Do not ask for confirmations.
5151
Useful for automated tasks.
5252

53+
.. option:: --keep
54+
55+
Don't delete news fragments after the build and don't ask for confirmation whether to delete or keep the fragments.
56+
5357

5458
``towncrier create``
5559
--------------------

src/towncrier/_git.py

+2-15
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,9 @@
77

88
from subprocess import STDOUT, call, check_output
99

10-
import click
1110

12-
13-
def remove_files(fragment_filenames: list[str], answer_yes: bool) -> None:
14-
if not fragment_filenames:
15-
return
16-
17-
if answer_yes:
18-
click.echo("Removing the following files:")
19-
else:
20-
click.echo("I want to remove the following files:")
21-
22-
for filename in fragment_filenames:
23-
click.echo(filename)
24-
25-
if answer_yes or click.confirm("Is it okay if I remove those files?", default=True):
11+
def remove_files(fragment_filenames: list[str]) -> None:
12+
if fragment_filenames:
2613
call(["git", "rm", "--quiet"] + fragment_filenames)
2714

2815

src/towncrier/build.py

+61-4
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616
import click
1717

18+
from click import Context, Option
19+
20+
from towncrier import _git
21+
1822
from ._builder import find_fragments, render_fragments, split_fragments
19-
from ._git import remove_files, stage_newsfile
2023
from ._project import get_project_name, get_version
2124
from ._settings import ConfigError, config_option_help, load_config_from_options
2225
from ._writer import append_to_newsfile
@@ -26,6 +29,18 @@ def _get_date() -> str:
2629
return date.today().isoformat()
2730

2831

32+
def _validate_answer(ctx: Context, param: Option, value: bool) -> bool:
33+
value_check = (
34+
ctx.params.get("answer_yes")
35+
if param.name == "answer_keep"
36+
else ctx.params.get("answer_keep")
37+
)
38+
if value_check and value:
39+
click.echo("You can not choose both --yes and --keep at the same time")
40+
ctx.abort()
41+
return value
42+
43+
2944
@click.command(name="build")
3045
@click.option(
3146
"--draft",
@@ -67,9 +82,18 @@ def _get_date() -> str:
6782
@click.option(
6883
"--yes",
6984
"answer_yes",
70-
default=False,
85+
default=None,
7186
flag_value=True,
7287
help="Do not ask for confirmation to remove news fragments.",
88+
callback=_validate_answer,
89+
)
90+
@click.option(
91+
"--keep",
92+
"answer_keep",
93+
default=None,
94+
flag_value=True,
95+
help="Do not ask for confirmations. But keep news fragments.",
96+
callback=_validate_answer,
7397
)
7498
def _main(
7599
draft: bool,
@@ -79,6 +103,7 @@ def _main(
79103
project_version: str | None,
80104
project_date: str | None,
81105
answer_yes: bool,
106+
answer_keep: bool,
82107
) -> None:
83108
"""
84109
Build a combined news file from news fragment.
@@ -92,6 +117,7 @@ def _main(
92117
project_version,
93118
project_date,
94119
answer_yes,
120+
answer_keep,
95121
)
96122
except ConfigError as e:
97123
print(e, file=sys.stderr)
@@ -106,6 +132,7 @@ def __main(
106132
project_version: str | None,
107133
project_date: str | None,
108134
answer_yes: bool,
135+
answer_keep: bool,
109136
) -> None:
110137
"""
111138
The main entry point.
@@ -234,13 +261,43 @@ def __main(
234261
)
235262

236263
click.echo("Staging newsfile...", err=to_err)
237-
stage_newsfile(base_directory, news_file)
264+
_git.stage_newsfile(base_directory, news_file)
238265

239266
click.echo("Removing news fragments...", err=to_err)
240-
remove_files(fragment_filenames, answer_yes)
267+
if should_remove_fragment_files(
268+
fragment_filenames,
269+
answer_yes,
270+
answer_keep,
271+
):
272+
_git.remove_files(fragment_filenames)
241273

242274
click.echo("Done!", err=to_err)
243275

244276

277+
def should_remove_fragment_files(
278+
fragment_filenames: list[str],
279+
answer_yes: bool,
280+
answer_keep: bool,
281+
) -> bool:
282+
try:
283+
if answer_keep:
284+
click.echo("Keeping the following files:")
285+
# Not proceeding with the removal of the files.
286+
return False
287+
288+
if answer_yes:
289+
click.echo("Removing the following files:")
290+
else:
291+
click.echo("I want to remove the following files:")
292+
finally:
293+
# Will always be printed, even for answer_keep to help with possible troubleshooting
294+
for filename in fragment_filenames:
295+
click.echo(filename)
296+
297+
if answer_yes or click.confirm("Is it okay if I remove those files?", default=True):
298+
return True
299+
return False
300+
301+
245302
if __name__ == "__main__": # pragma: no cover
246303
_main()
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added ``--keep`` option to the ``build`` command that allows to generate a newsfile, but keeps the newsfragments in place.
2+
This option can not be used together with ``--yes``.

src/towncrier/test/test_build.py

+61-1
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,66 @@ def test_no_confirmation(self):
407407
self.assertFalse(os.path.isfile(fragment_path1))
408408
self.assertFalse(os.path.isfile(fragment_path2))
409409

410+
@with_isolated_runner
411+
def test_keep_fragments(self, runner):
412+
"""
413+
The `--keep` option will build the full final news file
414+
without deleting the fragment files and without
415+
any extra CLI interaction or confirmation.
416+
"""
417+
setup_simple_project()
418+
fragment_path1 = "foo/newsfragments/123.feature"
419+
fragment_path2 = "foo/newsfragments/124.feature.rst"
420+
with open(fragment_path1, "w") as f:
421+
f.write("Adds levitation")
422+
with open(fragment_path2, "w") as f:
423+
f.write("Extends levitation")
424+
425+
call(["git", "init"])
426+
call(["git", "config", "user.name", "user"])
427+
call(["git", "config", "user.email", "user@example.com"])
428+
call(["git", "add", "."])
429+
call(["git", "commit", "-m", "Initial Commit"])
430+
431+
result = runner.invoke(_main, ["--date", "01-01-2001", "--keep"])
432+
433+
self.assertEqual(0, result.exit_code)
434+
# The NEWS file is created.
435+
# So this is not just `--draft`.
436+
self.assertTrue(os.path.isfile("NEWS.rst"))
437+
self.assertTrue(os.path.isfile(fragment_path1))
438+
self.assertTrue(os.path.isfile(fragment_path2))
439+
440+
@with_isolated_runner
441+
def test_yes_keep_error(self, runner):
442+
"""
443+
It will fail to perform any action when the
444+
conflicting --keep and --yes options are provided.
445+
446+
Called twice with the different order of --keep and --yes options
447+
to make sure both orders are validated since click triggers the validator
448+
in the order it parses the command line.
449+
"""
450+
setup_simple_project()
451+
fragment_path1 = "foo/newsfragments/123.feature"
452+
fragment_path2 = "foo/newsfragments/124.feature.rst"
453+
with open(fragment_path1, "w") as f:
454+
f.write("Adds levitation")
455+
with open(fragment_path2, "w") as f:
456+
f.write("Extends levitation")
457+
458+
call(["git", "init"])
459+
call(["git", "config", "user.name", "user"])
460+
call(["git", "config", "user.email", "user@example.com"])
461+
call(["git", "add", "."])
462+
call(["git", "commit", "-m", "Initial Commit"])
463+
464+
result = runner.invoke(_main, ["--date", "01-01-2001", "--yes", "--keep"])
465+
self.assertEqual(1, result.exit_code)
466+
467+
result = runner.invoke(_main, ["--date", "01-01-2001", "--keep", "--yes"])
468+
self.assertEqual(1, result.exit_code)
469+
410470
def test_confirmation_says_no(self):
411471
"""
412472
If the user says "no" to removing the newsfragements, we end up with
@@ -429,7 +489,7 @@ def test_confirmation_says_no(self):
429489
call(["git", "add", "."])
430490
call(["git", "commit", "-m", "Initial Commit"])
431491

432-
with patch("towncrier._git.click.confirm") as m:
492+
with patch("towncrier.build.click.confirm") as m:
433493
m.return_value = False
434494
result = runner.invoke(_main, [])
435495

0 commit comments

Comments
 (0)