Repository: twisted/towncrier
Pull request #475 (2023-02-09 01:39:51)

Make `towncrier create` increment fragments rather than failing
# Description
```bash
$ ls src/towncrier/newsfragments
459.misc  462.misc  470.doc  472.misc  
$ towncrier create 459.misc
Created news fragment at /home/chris/dev/towncrier/src/towncrier/newsfragments/459.misc.1
$ ls src/towncrier/newsfragments
459.misc  459.misc.1  462.misc  470.doc  472.misc
```

# Checklist
<!-- add a "X" inside the brackets to confirm -->
* [x] Make sure changes are covered by existing or new tests.
* [x] For at least one Python version, make sure local test run is green.
* [x] Create a file in `src/towncrier/newsfragments/`. Describe your
  change and include important information. Your change will be included in the public release notes.
* [x] Make sure all GitHub Actions checks are green (they are automatically checking all of the above).
* [x] Ensure `docs/tutorial.rst` is still up-to-date.


codecov[bot]: # [Codecov](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) Report
Base: **100.00**% // Head: **99.68**% // Decreases project coverage by **`-0.32%`** :warning:
> Coverage data is based on head [(`6f86444`)](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) compared to base [(`53dd3e2`)](https://codecov.io/gh/twisted/towncrier/commit/53dd3e2cce1b64aff2ae1cb9d9f3c906836fb71a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).
> Patch coverage: 100.00% of modified lines in pull request are covered.

> :exclamation: Current head 6f86444 differs from pull request most recent head 811ce1e. Consider uploading reports for the commit 811ce1e to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             trunk     #475      +/-   ##
===========================================
- Coverage   100.00%   99.68%   -0.32%     
===========================================
  Files           13       13              
  Lines          621      626       +5     
  Branches       144      117      -27     
===========================================
+ Hits           621      624       +3     
- Misses           0        1       +1     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) | Coverage Δ | |
|---|---|---|
| [src/towncrier/create.py](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted#diff-c3JjL3Rvd25jcmllci9jcmVhdGUucHk=) | `100.00% <100.00%> (ø)` | |
| [src/towncrier/\_settings/load.py](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted#diff-c3JjL3Rvd25jcmllci9fc2V0dGluZ3MvbG9hZC5weQ==) | `97.91% <0.00%> (-2.09%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/twisted/towncrier/pull/475?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted).

hynek: I think `1234-1.bugfix` is better too, because part of my secret todo list it `123,456.bugfix` so I don’t have to keep multiple news fragments in sync, just to have an entry point to multiple issues. `123-1,456.bugfix` would work because it’s unequivocal.

That said, I don’t see much sense in that, you can just edit the old news fragment or is this about pacifying bots that _require_ a new news fragment?
pawelad: Isn't this just adding support for an _already implemented_ counter feature (https://github.com/twisted/towncrier/search?q=counter) to the `towncrier create` command?
adiroiban: Thanks @pawelad for the head up.

I can see that the "counter" functionality was added here https://github.com/twisted/towncrier/pull/119

I think that the current team of maintainers is not aware of that functionality, and we would all like not to have the counter as a "file extension".

It looks like the counter feature was added without documentation...but I think that by then, there was no documentation for towncrier.

So, I think that this should be merged with the counter as an extension.

Later, we can see if we can refactor this not to have the counter as an extension.

I think that towncrier also has support for `459.misc.rst` ... if the `counter` feature works as `459.misc.1.rst` I think that we are fine. 

SmileyChris: Ok, I've fixed those minor suggestions for you @adiroiban.

As you've discovered, this is just functionality that towncrier has already, being surfaced more clearly. Hopefully @glyph agrees, I've re-requested him to review.

Patch:
diff --git a/docs/cli.rst b/docs/cli.rst
index b32db63..22697d0 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 a16906f..5242e12 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 0000000..aac8607
--- /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``.


Test patch:
diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py
index a6df6b3..bb33da4 100644
--- a/src/towncrier/test/test_create.py
+++ b/src/towncrier/test/test_create.py
@@ -151,20 +151,48 @@ class TestCli(TestCase):
             "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):