From a651d175b38109138007c501f96a9c04c60959e2 Mon Sep 17 00:00:00 2001 From: Sagi Shadur Date: Wed, 2 Dec 2020 23:37:20 +0200 Subject: [PATCH] Users can now write the content in the "create" method. (#275) * Users can now write the content in the "create" method. * Add missing news fragment * Add missing test for interactive content * Use the click.edit method in order to write content * Add context and exit code to create.py abort * Add missing unit tests to the create method. * Update src/towncrier/newsfragments/275.feature Co-authored-by: Kyle Altendorf * Fix mock import error * Fix code after review * Apply suggestions from code review Co-authored-by: Kyle Altendorf * refactor _get_content method Co-authored-by: Kyle Altendorf --- src/towncrier/create.py | 32 +++++++++++++++--- src/towncrier/newsfragments/275.feature | 1 + src/towncrier/test/test_create.py | 43 +++++++++++++++++++++++-- tox.ini | 1 + 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/towncrier/newsfragments/275.feature diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 85fc95b0..222f615b 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -14,14 +14,16 @@ @click.command(name="create") +@click.pass_context @click.option("--dir", "directory", default=None) @click.option("--config", "config", default=None) +@click.option("--edit/--no-edit", default=False, help="Open an editor for writing the newsfragment content.") # TODO: default should be true @click.argument("filename") -def _main(directory, config, filename): - return __main(directory, config, filename) +def _main(ctx, directory, config, filename, edit): + return __main(ctx, directory, config, filename, edit) -def __main(directory, config, filename): +def __main(ctx, directory, config, filename, edit): """ The main entry point. """ @@ -59,11 +61,33 @@ def __main(directory, config, filename): if os.path.exists(segment_file): raise click.ClickException("{} already exists".format(segment_file)) + if edit: + content = _get_news_content_from_user() + else: + content = "Add your info here" + + if content is None: + click.echo("Abort creating news fragment.") + ctx.exit(1) + with open(segment_file, "w") as f: - f.writelines(["Add your info here"]) + f.write(content) click.echo("Created news fragment at {}".format(segment_file)) +def _get_news_content_from_user(): + content = click.edit( + "# Please write your news content. When finished, save the file.\n" + "# In order to abort, exit without saving.\n" + "# Lines starting with \"#\" are ignored.\n" + ) + if content is None: + return None + all_lines = content.split("\n") + lines = [line.rstrip() for line in all_lines if not line.lstrip().startswith("#")] + return "\n".join(lines) + + if __name__ == "__main__": # pragma: no cover _main() diff --git a/src/towncrier/newsfragments/275.feature b/src/towncrier/newsfragments/275.feature new file mode 100644 index 00000000..413b9dbe --- /dev/null +++ b/src/towncrier/newsfragments/275.feature @@ -0,0 +1 @@ +The new ``--edit`` option of the ``create`` subcommand launches an editor for entering the contents of the newsfragment. diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index b53ceed1..f6790f82 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -3,7 +3,9 @@ import os from textwrap import dedent + from twisted.trial.unittest import TestCase +import mock from click.testing import CliRunner @@ -33,18 +35,25 @@ def setup_simple_project(config=None, mkdir=True): class TestCli(TestCase): maxDiff = None - def _test_success(self, config=None, mkdir=True): + def _test_success( + self, content=None, config=None, mkdir=True, additional_args=None + ): runner = CliRunner() with runner.isolated_filesystem(): setup_simple_project(config, mkdir) - result = runner.invoke(_main, ["123.feature.rst"]) + args = ["123.feature.rst"] + if content is None: + content = ["Add your info here"] + if additional_args is not None: + args.extend(additional_args) + result = runner.invoke(_main, args) self.assertEqual(["123.feature.rst"], os.listdir("foo/newsfragments")) with open("foo/newsfragments/123.feature.rst") as fh: - self.assertEqual("Add your info here", fh.readlines()[0]) + self.assertEqual(content, fh.readlines()) self.assertEqual(0, result.exit_code) @@ -56,6 +65,34 @@ def test_directory_created(self): """Ensure both file and output directory created if necessary.""" self._test_success(mkdir=False) + def test_edit_without_comments(self): + """Create file with dynamic content.""" + content = ["This is line 1\n", "This is line 2"] + with mock.patch("click.edit") as mock_edit: + mock_edit.return_value = "".join(content) + self._test_success(content=content, additional_args=["--edit"]) + + def test_edit_with_comment(self): + """Create file editly with ignored line.""" + content = ["This is line 1\n", "This is line 2"] + comment = "# I am ignored\n" + with mock.patch("click.edit") as mock_edit: + mock_edit.return_value = "".join(content[:1] + [comment] + content[1:]) + self._test_success(content=content, additional_args=["--edit"]) + + def test_edit_abort(self): + """Create file editly and abort.""" + with mock.patch("click.edit") as mock_edit: + mock_edit.return_value = None + + runner = CliRunner() + + with runner.isolated_filesystem(): + setup_simple_project(config=None, mkdir=True) + result = runner.invoke(_main, ["123.feature.rst", "--edit"]) + self.assertEqual([], os.listdir("foo/newsfragments")) + self.assertEqual(1, result.exit_code) + def test_different_directory(self): """Ensure non-standard directories are used.""" runner = CliRunner() diff --git a/tox.ini b/tox.ini index d99ec4b7..8c605254 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,7 @@ deps = Twisted coverage incremental + mock commands = python -V