From 826beacb7e96a3360ed9e5153e4be06dd74b028d Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Tue, 21 Feb 2023 09:34:37 +1300 Subject: [PATCH] Make `towncrier create` increment fragments rather than failing (#475) * Make `towncrier create` increment fragments rather than failing Fixes #474 * Rename change file * Add example to tutorial of incrementing via towncrier create * Remove the counter example from the tutorial, this isn't core functionality * Better docstring for the new test --------- Co-authored-by: Glyph --- docs/cli.rst | 3 ++ src/towncrier/create.py | 13 +++++-- src/towncrier/newsfragments/475.feature | 6 ++++ src/towncrier/test/test_create.py | 46 ++++++++++++++++++++----- 4 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 src/towncrier/newsfragments/475.feature diff --git a/docs/cli.rst b/docs/cli.rst index b32db637..22697d01 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -64,6 +64,9 @@ Create a news fragment in the directory that ``towncrier`` is configured to look ``towncrier create`` will enforce that the passed type (e.g. ``bugfix``) is valid. +If the filename exists already, ``towncrier create`` will add (and then increment) a number after the fragment type until it finds a filename that does not exist yet. +In the above example, it will generate ``123.bugfix.1.rst`` if ``123.bugfix.rst`` already exists. + .. option:: --content, -c CONTENT A string to use for content. diff --git a/src/towncrier/create.py b/src/towncrier/create.py index a16906fe..5242e124 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -119,8 +119,17 @@ def __main( os.makedirs(fragments_directory) segment_file = os.path.join(fragments_directory, filename) - if os.path.exists(segment_file): - raise click.ClickException(f"{segment_file} already exists") + + retry = 0 + if filename.split(".")[-1] not in config.types: + filename, extra_ext = os.path.splitext(filename) + else: + extra_ext = "" + while os.path.exists(segment_file): + retry += 1 + segment_file = os.path.join( + fragments_directory, f"{filename}.{retry}{extra_ext}" + ) if edit: edited_content = _get_news_content_from_user(content) diff --git a/src/towncrier/newsfragments/475.feature b/src/towncrier/newsfragments/475.feature new file mode 100644 index 00000000..aac86074 --- /dev/null +++ b/src/towncrier/newsfragments/475.feature @@ -0,0 +1,6 @@ +Make ``towncrier create`` use the fragment counter rather than failing +on existing fragment names. + +For example, if there is an existing fragment named ``123.feature``, +then ``towncrier create 123.feature`` will now create a fragment +named ``123.feature.1``. diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index a6df6b31..bb33da4c 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -151,20 +151,48 @@ def test_invalid_section(self): "Expected filename '123.foobar.rst' to be of format", result.output ) - def test_file_exists(self): + @with_isolated_runner + def test_file_exists(self, runner: CliRunner): """Ensure we don't overwrite existing files.""" - runner = CliRunner() + setup_simple_project() + frag_path = Path("foo", "newsfragments") - with runner.isolated_filesystem(): - setup_simple_project() + for _ in range(3): + result = runner.invoke(_main, ["123.feature"]) + self.assertEqual(result.exit_code, 0, result.output) + + fragments = [f.name for f in frag_path.iterdir()] + self.assertEqual( + sorted(fragments), + [ + "123.feature", + "123.feature.1", + "123.feature.2", + ], + ) - self.assertEqual([], os.listdir("foo/newsfragments")) + @with_isolated_runner + def test_file_exists_with_ext(self, runner: CliRunner): + """ + Ensure we don't overwrite existing files when using an extension after the + fragment type. + """ + setup_simple_project() + frag_path = Path("foo", "newsfragments") - runner.invoke(_main, ["123.feature.rst"]) + for _ in range(3): result = runner.invoke(_main, ["123.feature.rst"]) - - self.assertEqual(type(result.exception), SystemExit) - self.assertIn("123.feature.rst already exists", result.output) + self.assertEqual(result.exit_code, 0, result.output) + + fragments = [f.name for f in frag_path.iterdir()] + self.assertEqual( + sorted(fragments), + [ + "123.feature.1.rst", + "123.feature.2.rst", + "123.feature.rst", + ], + ) @with_isolated_runner def test_create_orphan_fragment(self, runner: CliRunner):