Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
-------------------
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/click_default_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def bar():
bar

"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this change needed?

@tirolerstefan tirolerstefan Jun 9, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was one of the "auto fixes" of pre-commit.
no, it's not needed in my eyes, but i have to commit it using "--no-verify".
if you don't mind, i will leave it as it is to satisfy the pre-commit tools.

import warnings

import click
Expand Down
27 changes: 22 additions & 5 deletions src/towncrier/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@
type=str,
help="The section to create the fragment for.",
)
@click.option(
"--index",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Index is kind of generic...
Maybe it's best to be more explicit to prevent future conflicts and confusions/

Suggested change
"--index",
"--issue-index",

I don't know if "index" is the best name for this feature... maybe we can call it --sub-issue or someting like that

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm, it's like an index - 0 is the first fragment, 1 the next, etc. but i can live with any text.
"sub-issue" is totally ok, "sub-fragment" or "fragment-number", "fragment-id", "sub-id"?

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,
Expand All @@ -63,6 +70,7 @@ def _main(
edit: bool | None,
content: str,
section: str | None,
index: int | None,
) -> None:
"""
Create a new news fragment.
Expand All @@ -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(
Expand All @@ -94,6 +102,7 @@ def __main(
edit: bool | None,
content: str,
section: str | None,
index: int | None,
) -> None:
"""
The main entry point.
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/towncrier/newsfragments/714.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
95 changes: 95 additions & 0 deletions src/towncrier/test/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Loading