Repository: twisted/towncrier
Pull request #468 (2023-01-17 01:07:10)

Fix `create` for a bare orphan in a section not adding random hex
# Description

When running `towncrier create section/+.feature`, the random hex isn't appended.

# 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/468?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: **100.00**% // No change to project coverage :thumbsup:
> Coverage data is based on head [(`15b8a60`)](https://codecov.io/gh/twisted/towncrier/pull/468?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=twisted) compared to base [(`ffcc117`)](https://codecov.io/gh/twisted/towncrier/commit/ffcc117225504d7275a9662be421d1494c33e907?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.

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


```diff
@@            Coverage Diff            @@
##             trunk      #468   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           13        13           
  Lines          613       614    +1     
  Branches       143       143           
=========================================
+ Hits           613       614    +1     
```


| [Impacted Files](https://codecov.io/gh/twisted/towncrier/pull/468?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/468?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%> (ø)` | |

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/468?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).

SmileyChris: Test updated, and new test added to check for custom orphan prefix (of different length)

Patch:
diff --git a/src/towncrier/create.py b/src/towncrier/create.py
index 92895a8..a16906f 100644
--- a/src/towncrier/create.py
+++ b/src/towncrier/create.py
@@ -81,9 +81,16 @@ def __main(
     """
     base_directory, config = load_config_from_options(directory, config_path)
 
-    if config.orphan_prefix and filename.startswith(f"{config.orphan_prefix}."):
+    file_dir, file_basename = os.path.split(filename)
+    if config.orphan_prefix and file_basename.startswith(f"{config.orphan_prefix}."):
         # Append a random hex string to the orphan news fragment base name.
-        filename = f"{config.orphan_prefix}{os.urandom(4).hex()}{filename[1:]}"
+        filename = os.path.join(
+            file_dir,
+            (
+                f"{config.orphan_prefix}{os.urandom(4).hex()}"
+                f"{file_basename[len(config.orphan_prefix):]}"
+            ),
+        )
     if len(filename.split(".")) < 2 or (
         filename.split(".")[-1] not in config.types
         and filename.split(".")[-2] not in config.types
diff --git a/src/towncrier/newsfragments/468.bugfix b/src/towncrier/newsfragments/468.bugfix
new file mode 100644
index 0000000..8bac034
--- /dev/null
+++ b/src/towncrier/newsfragments/468.bugfix
@@ -0,0 +1,3 @@
+Fix creating fragment in a section not adding random characters.
+
+For example, ``towncrier create some_section/+.feature`` should end up as a fragment named something like ``news/some_section/+a4e22da1.feature``.


Test patch:
diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py
index e2af46c..a6df6b3 100644
--- a/src/towncrier/test/test_create.py
+++ b/src/towncrier/test/test_create.py
@@ -2,7 +2,9 @@
 # See LICENSE for details.
 
 import os
+import string
 
+from pathlib import Path
 from textwrap import dedent
 from unittest import mock
 
@@ -10,7 +12,7 @@ from click.testing import CliRunner
 from twisted.trial.unittest import TestCase
 
 from ..create import _main
-from .helpers import setup_simple_project
+from .helpers import setup_simple_project, with_isolated_runner
 
 
 class TestCli(TestCase):
@@ -164,25 +166,58 @@ class TestCli(TestCase):
         self.assertEqual(type(result.exception), SystemExit)
         self.assertIn("123.feature.rst already exists", result.output)
 
-    def test_create_orphan_fragment(self):
+    @with_isolated_runner
+    def test_create_orphan_fragment(self, runner: CliRunner):
         """
         When a fragment starts with the only the orphan prefix (``+`` by default), the
         create CLI automatically extends the new file's base name to contain a random
         value to avoid commit collisions.
         """
-        runner = CliRunner()
+        setup_simple_project()
 
-        with runner.isolated_filesystem():
-            setup_simple_project()
+        frag_path = Path("foo", "newsfragments")
+        sub_frag_path = frag_path / "subsection"
+        sub_frag_path.mkdir()
 
-            self.assertEqual([], os.listdir("foo/newsfragments"))
+        result = runner.invoke(_main, ["+.feature"])
+        self.assertEqual(0, result.exit_code)
+        result = runner.invoke(
+            _main, [str(Path("subsection", "+.feature"))], catch_exceptions=False
+        )
+        self.assertEqual(0, result.exit_code, result.output)
+
+        fragments = [p for p in frag_path.rglob("*") if p.is_file()]
+        self.assertEqual(2, len(fragments))
+        change1, change2 = fragments
 
-            runner.invoke(_main, ["+.feature"])
-            fragments = os.listdir("foo/newsfragments")
+        self.assertEqual(change1.suffix, ".feature")
+        self.assertTrue(change1.stem.startswith("+"))
+        # Length should be '+' character and 8 random hex characters.
+        self.assertEqual(len(change1.stem), 9)
 
-        self.assertEqual(1, len(fragments))
-        filename = fragments[0]
-        self.assertTrue(filename.endswith(".feature"))
-        self.assertTrue(filename.startswith("+"))
+        self.assertEqual(change2.suffix, ".feature")
+        self.assertTrue(change2.stem.startswith("+"))
+        self.assertEqual(change2.parent, sub_frag_path)
         # Length should be '+' character and 8 random hex characters.
-        self.assertEqual(len(filename.split(".")[0]), 9)
+        self.assertEqual(len(change2.stem), 9)
+
+    @with_isolated_runner
+    def test_create_orphan_fragment_custom_prefix(self, runner: CliRunner):
+        """
+        Check that the orphan prefix can be customized.
+        """
+        setup_simple_project(extra_config='orphan_prefix = "$$$"')
+
+        frag_path = Path("foo", "newsfragments")
+
+        result = runner.invoke(_main, ["$$$.feature"])
+        self.assertEqual(0, result.exit_code, result.output)
+
+        fragments = list(frag_path.rglob("*"))
+        self.assertEqual(len(fragments), 1)
+        change = fragments[0]
+        self.assertTrue(change.stem.startswith("$$$"))
+        # Length should be '$$$' characters and 8 random hex characters.
+        self.assertEqual(len(change.stem), 11)
+        # Check the remainder are all hex characters.
+        self.assertTrue(all(c in string.hexdigits for c in change.stem[3:]))