diff --git a/docs/cli.rst b/docs/cli.rst index 12baf1d8..4535033c 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -104,6 +104,12 @@ If that is the entire fragment name, a random hash will be added for you:: The section to use for the news fragment. Default: the section with no path, or if all sections have a path then the first defined section. +.. option:: --index N + + Optional numeric index of the fragment, N>=0. + e.g. x=0 -> 'issue.feat.md' is accessed and overwritten + e.g. x=1 -> 'issue.feat.1.md' is accessed and overwritten + ``towncrier check`` ------------------- diff --git a/src/towncrier/click_default_group.py b/src/towncrier/click_default_group.py index a50cce0d..db52c541 100644 --- a/src/towncrier/click_default_group.py +++ b/src/towncrier/click_default_group.py @@ -52,6 +52,7 @@ def bar(): bar """ + import warnings import click diff --git a/src/towncrier/create.py b/src/towncrier/create.py index e78fb658..96cb9705 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -54,6 +54,13 @@ type=str, help="The section to create the fragment for.", ) +@click.option( + "--index", + type=click.IntRange(min=0), + metavar="N", + default=None, + help="Optional numeric index of the fragment", +) @click.argument("filename", default="") def _main( ctx: click.Context, @@ -63,6 +70,7 @@ def _main( edit: bool | None, content: str, section: str | None, + index: int | None, ) -> None: """ Create a new news fragment. @@ -83,7 +91,7 @@ def _main( If the FILENAME base is just '+' (to create a fragment not tied to an issue), it will be appended with a random hex string. """ - __main(ctx, directory, config, filename, edit, content, section) + __main(ctx, directory, config, filename, edit, content, section, index) def __main( @@ -94,6 +102,7 @@ def __main( edit: bool | None, content: str, section: str | None, + index: int | None, ) -> None: """ The main entry point. @@ -187,6 +196,7 @@ def __main( "where '{{name}}' is an arbitrary slug and '{{type}}' is " "one of: {}".format(filename, ", ".join(config.types)) ) + if filename_parts[-1] in config.types and filename_ext: filename += filename_ext @@ -198,15 +208,22 @@ def __main( segment_file = os.path.join(fragments_directory, filename) - 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 + + if index is None: + retry = 0 + while os.path.exists(segment_file): + retry += 1 + segment_file = os.path.join( + fragments_directory, f"{filename}.{retry}{extra_ext}" + ) + else: segment_file = os.path.join( - fragments_directory, f"{filename}.{retry}{extra_ext}" + fragments_directory, + f"{filename}{'.' + str(index) if index > 0 else ''}{extra_ext}", ) if edit: diff --git a/src/towncrier/newsfragments/714.feature.rst b/src/towncrier/newsfragments/714.feature.rst new file mode 100644 index 00000000..50b65f70 --- /dev/null +++ b/src/towncrier/newsfragments/714.feature.rst @@ -0,0 +1 @@ +The `towncrier create` command line tool now has the `--index` option that is used when creating multiple fragments for the same issue number. diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index dc6f6b9d..4a208c78 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -667,3 +667,98 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): self.assertEqual(0, result.exit_code) self.assertTrue(Path("foo/changelog.d/123.feature.rst").exists()) + + @with_isolated_runner + def test_index(self, runner): + """ + testing changing files with option --index + """ + Path("pyproject.toml").write_text( + # Important to customize `config.directory` because the default + # already supports this scenario. + "[tool.towncrier]\n" + 'directory = "changelog.d"\n' + ) + Path("foo/foo").mkdir(parents=True) + result = runner.invoke( + _main, + ( + "--config", + "pyproject.toml", + "--dir", + "foo", + "--content", + "111", + "--index", + 0, + "123.feature", + ), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/changelog.d/123.feature.rst").exists()) + + with open("foo/changelog.d/123.feature.rst") as fh: + self.assertEqual("111\n", fh.read()) + + result = runner.invoke( + _main, + ( + "--config", + "pyproject.toml", + "--dir", + "foo", + "--content", + "222", + "--index", + 1, + "123.feature", + ), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/changelog.d/123.feature.1.rst").exists()) + + with open("foo/changelog.d/123.feature.1.rst") as fh: + self.assertEqual("222\n", fh.read()) + + result = runner.invoke( + _main, + ( + "--config", + "pyproject.toml", + "--dir", + "foo", + "--content", + "333", + "--index", + 0, + "123.feature", + ), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/changelog.d/123.feature.rst").exists()) + + with open("foo/changelog.d/123.feature.rst") as fh: + self.assertEqual("333\n", fh.read()) + + result = runner.invoke( + _main, + ( + "--config", + "pyproject.toml", + "--dir", + "foo", + "--content", + "444", + "--index", + 1, + "123.feature", + ), + ) + + self.assertEqual(0, result.exit_code) + self.assertTrue(Path("foo/changelog.d/123.feature.1.rst").exists()) + + with open("foo/changelog.d/123.feature.1.rst") as fh: + self.assertEqual("444\n", fh.read())