From 474f11a43ae767129c9b67bd9bc16ff6cf80710e Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Wed, 22 May 2024 16:49:18 +1200 Subject: [PATCH 01/13] Abstract the fragments path generation --- src/towncrier/_builder.py | 49 ++++++++++++++++++++++++++++++--------- src/towncrier/build.py | 19 +-------------- src/towncrier/check.py | 19 +-------------- src/towncrier/create.py | 15 ++---------- 4 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index 3a4591de..944d9d7e 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -13,6 +13,8 @@ from jinja2 import Template +from towncrier._settings.load import Config + # Returns ticket, category and counter or (None, None, None) if the basename # could not be parsed or doesn't contain a valid category. @@ -53,6 +55,35 @@ def parse_newfragment_basename( return invalid +class FragmentsPath: + """ + A helper to get the full path to a fragments directory. + + This is a callable that optionally takes a section directory and returns the full + path to the fragments directory for that section (or the default if no section is + provided). + """ + + def __init__(self, base_directory: str, config: Config): + self.base_directory = base_directory + self.config = config + if config.directory is not None: + self.base_directory = os.path.abspath( + os.path.join(base_directory, config.directory) + ) + self.append_directory = "" + else: + self.base_directory = os.path.abspath( + os.path.join(base_directory, config.package_dir, config.package) + ) + self.append_directory = "newsfragments" + + def __call__(self, section_directory: str = "") -> str: + return os.path.join( + self.base_directory, section_directory, self.append_directory + ) + + # Returns a structure like: # # { @@ -69,25 +100,21 @@ def parse_newfragment_basename( # Also returns a list of the paths that the fragments were taken from. def find_fragments( base_directory: str, - sections: Mapping[str, str], - fragment_directory: str | None, - frag_type_names: Iterable[str], - orphan_prefix: str | None = None, + config: Config, ) -> tuple[Mapping[str, Mapping[tuple[str, str, int], str]], list[str]]: """ Sections are a dictonary of section names to paths. """ + get_section_path = FragmentsPath(base_directory, config) + content = {} fragment_filenames = [] # Multiple orphan news fragments are allowed per section, so initialize a counter # that can be incremented automatically. orphan_fragment_counter: DefaultDict[str | None, int] = defaultdict(int) - for key, val in sections.items(): - if fragment_directory is not None: - section_dir = os.path.join(base_directory, val, fragment_directory) - else: - section_dir = os.path.join(base_directory, val) + for key, section_dir in config.sections.items(): + section_dir = get_section_path(section_dir) try: files = os.listdir(section_dir) @@ -98,13 +125,13 @@ def find_fragments( for basename in files: ticket, category, counter = parse_newfragment_basename( - basename, frag_type_names + basename, config.types ) if category is None: continue assert ticket is not None assert counter is not None - if orphan_prefix and ticket.startswith(orphan_prefix): + if config.orphan_prefix and ticket.startswith(config.orphan_prefix): ticket = "" # Use and increment the orphan news fragment counter. counter = orphan_fragment_counter[category] diff --git a/src/towncrier/build.py b/src/towncrier/build.py index b28606c9..7ca44c88 100644 --- a/src/towncrier/build.py +++ b/src/towncrier/build.py @@ -175,24 +175,7 @@ def __main( click.echo("Finding news fragments...", err=to_err) - if config.directory is not None: - fragment_base_directory = os.path.abspath( - os.path.join(base_directory, config.directory) - ) - fragment_directory = None - else: - fragment_base_directory = os.path.abspath( - os.path.join(base_directory, config.package_dir, config.package) - ) - fragment_directory = "newsfragments" - - fragment_contents, fragment_filenames = find_fragments( - fragment_base_directory, - config.sections, - fragment_directory, - config.types, - config.orphan_prefix, - ) + fragment_contents, fragment_filenames = find_fragments(base_directory, config) click.echo("Rendering news fragments...", err=to_err) fragments = split_fragments( diff --git a/src/towncrier/check.py b/src/towncrier/check.py index ee9b612e..f0d45677 100644 --- a/src/towncrier/check.py +++ b/src/towncrier/check.py @@ -106,25 +106,8 @@ def __main( click.echo("Checks SKIPPED: news file changes detected.") sys.exit(0) - if config.directory: - fragment_base_directory = os.path.abspath( - os.path.join(base_directory, config.directory) - ) - fragment_directory = None - else: - fragment_base_directory = os.path.abspath( - os.path.join(base_directory, config.package_dir, config.package) - ) - fragment_directory = "newsfragments" - fragments = { - os.path.abspath(path) - for path in find_fragments( - fragment_base_directory, - config.sections, - fragment_directory, - config.types.keys(), - )[1] + os.path.abspath(path) for path in find_fragments(base_directory, config)[1] } fragments_in_branch = fragments & files diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 77433fca..4cccc409 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -134,19 +134,8 @@ def __main( if filename_parts[-1] in config.types and filename_ext: filename += filename_ext - if config.directory: - fragments_directory = os.path.abspath( - os.path.join(base_directory, config.directory) - ) - else: - fragments_directory = os.path.abspath( - os.path.join( - base_directory, - config.package_dir, - config.package, - "newsfragments", - ) - ) + get_fragments_path = FragmentsPath(base_directory, config) + fragments_directory = get_fragments_path(section_directory=config.sections[section]) if not os.path.exists(fragments_directory): os.makedirs(fragments_directory) From 25e6c3ceb2686f65e9f112f26f89e1368f7a419f Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Wed, 22 May 2024 16:53:24 +1200 Subject: [PATCH 02/13] Update the create option to work with sections --- docs/cli.rst | 5 ++ src/towncrier/create.py | 58 +++++++++++++++- src/towncrier/test/test_create.py | 107 ++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) diff --git a/docs/cli.rst b/docs/cli.rst index 478a88b9..108a5429 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -98,6 +98,11 @@ If that is the entire fragment name, a random hash will be added for you:: Whether to start ``$EDITOR`` to edit the news fragment right away. Default: ``$EDITOR`` will be started unless you also provided content. +.. option:: --section SECTION + + The section to use for the news fragment. + Default: the first section with no path. + ``towncrier check`` ------------------- diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 4cccc409..fca96c00 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -13,6 +13,7 @@ import click +from ._builder import FragmentsPath from ._settings import config_option_help, load_config_from_options @@ -47,6 +48,11 @@ default=DEFAULT_CONTENT, help="Sets the content of the new fragment.", ) +@click.option( + "--section", + type=str, + help="The section to create the fragment for.", +) @click.argument("filename", default="") def _main( ctx: click.Context, @@ -55,6 +61,7 @@ def _main( filename: str, edit: bool | None, content: str, + section: str | None, ) -> None: """ Create a new news fragment. @@ -75,7 +82,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) + __main(ctx, directory, config, filename, edit, content, section) def __main( @@ -85,6 +92,7 @@ def __main( filename: str, edit: bool | None, content: str, + section: str | None, ) -> None: """ The main entry point. @@ -97,7 +105,47 @@ def __main( if ext.lower() in (".rst", ".md"): filename_ext = ext + # Get the default section. + default_section = None + if len(config.sections) == 1: + default_section = next(iter(config.sections)) + else: + # If there are mulitple sections then the first without a path is the default + # section, otherwise there's no default. + for section_name, section_dir in config.sections.items(): + if not section_dir: + default_section = section_name + break + + if section is not None: + if section not in config.sections: + section_param = None + for p in ctx.command.params: # pragma: no branch + if p.name == "section": + section_param = p + break + expected_sections = ", ".join(f"'{s}'" for s in config.sections) + raise click.BadParameter( + f"expected one of {expected_sections}", + param=section_param, + ) + if not filename: + if section is None: + sections = list(config.sections) + if len(sections) > 1: + click.echo("Pick a section:") + default_section_index = None + for i, section in enumerate(sections): + click.echo(f" {i+1}: {section or '(primary)'}") + if not default_section_index and section == default_section: + default_section_index = str(i + 1) + section_index = click.prompt( + "Section", + type=click.Choice([str(i + 1) for i in range(len(sections))]), + default=default_section_index, + ) + section = sections[int(section_index) - 1] prompt = "Issue number" # Add info about adding orphan if config is set. if config.orphan_prefix: @@ -134,6 +182,14 @@ def __main( if filename_parts[-1] in config.types and filename_ext: filename += filename_ext + if not section: + if default_section is None: + raise click.UsageError( + "Multiple sections defined in configuration file, all with paths." + " Please define a section with `--section`." + ) + section = default_section + get_fragments_path = FragmentsPath(base_directory, config) fragments_directory = get_fragments_path(section_directory=config.sections[section]) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 2ba74af9..65cbbc6d 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -415,6 +415,113 @@ def test_without_filename_no_orphan_config(self, runner: CliRunner): with open(expected) as f: self.assertEqual(f.read(), "Edited content\n") + @with_isolated_runner + def test_sections(self, runner: CliRunner): + setup_simple_project( + extra_config=""" +[[tool.towncrier.section]] +name = "Backend" +path = "backend" +[[tool.towncrier.section]] +name = "Frontend" +path = "frontend" +""" + ) + result = runner.invoke(_main, ["123.feature.rst"]) + self.assertTrue(result.exception, result.output) + self.assertEqual( + result.output, + """\ +Usage: create [OPTIONS] [FILENAME] +Try 'create --help' for help. + +Error: Multiple sections defined in configuration file, all with paths.\ + Please define a section with `--section`. +""", + ) + + result = runner.invoke(_main, ["123.feature.rst", "--section", "invalid"]) + self.assertTrue(result.exception, result.output) + self.assertIn( + "Invalid value for '--section': expected one of 'Backend', 'Frontend'", + result.output, + ) + + result = runner.invoke(_main, ["123.feature.rst", "--section", "Frontend"]) + self.assertFalse(result.exception, result.output) + frag_path = Path("foo", "frontend", "newsfragments") + + fragments = [f.name for f in frag_path.iterdir()] + self.assertEqual(fragments, ["123.feature.rst"]) + + @with_isolated_runner + def test_sections_without_filename(self, runner: CliRunner): + setup_simple_project( + extra_config=""" +[[tool.towncrier.section]] +name = "Backend" +path = "" + +[[tool.towncrier.section]] +name = "Frontend" +path = "frontend" +""" + ) + with mock.patch("click.edit") as mock_edit: + mock_edit.return_value = "Edited content" + result = runner.invoke(_main, input="2\n123\nfeature\n") + self.assertFalse(result.exception, result.output) + mock_edit.assert_called_once() + expected = os.path.join( + os.getcwd(), "foo", "frontend", "newsfragments", "123.feature.rst" + ) + + self.assertEqual( + result.output, + f"""\ +Pick a section: + 1: Backend + 2: Frontend +Section (1, 2) [1]: 2 +Issue number (`+` if none): 123 +Fragment type (feature, bugfix, doc, removal, misc): feature +Created news fragment at {expected} +""", + ) + + @with_isolated_runner + def test_sections_without_filename_with_section_option(self, runner: CliRunner): + setup_simple_project( + extra_config=""" +[[tool.towncrier.section]] +name = "Backend" +path = "" + +[[tool.towncrier.section]] +name = "Frontend" +path = "frontend" +""" + ) + with mock.patch("click.edit") as mock_edit: + mock_edit.return_value = "Edited content" + result = runner.invoke( + _main, ["--section", "Frontend"], input="123\nfeature\n" + ) + self.assertFalse(result.exception, result.output) + mock_edit.assert_called_once() + expected = os.path.join( + os.getcwd(), "foo", "frontend", "newsfragments", "123.feature.rst" + ) + + self.assertEqual( + result.output, + f"""\ +Issue number (`+` if none): 123 +Fragment type (feature, bugfix, doc, removal, misc): feature +Created news fragment at {expected} +""", + ) + @with_isolated_runner def test_without_filename_with_message(self, runner: CliRunner): """ From 103d3730a7460a2ec7df6a5a6f7c24ecac06b9e1 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Wed, 22 May 2024 16:58:36 +1200 Subject: [PATCH 03/13] Add fragment --- src/towncrier/newsfragments/+0732bd4a.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/towncrier/newsfragments/+0732bd4a.feature.rst diff --git a/src/towncrier/newsfragments/+0732bd4a.feature.rst b/src/towncrier/newsfragments/+0732bd4a.feature.rst new file mode 100644 index 00000000..bc13c913 --- /dev/null +++ b/src/towncrier/newsfragments/+0732bd4a.feature.rst @@ -0,0 +1 @@ +The `towncrier create` action is now content aware (either interactively, or with a new `--section` option). From 5f5bd00b657fcc87e4446e6d8d6e83c500d58db5 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Fri, 24 May 2024 11:43:34 +1200 Subject: [PATCH 04/13] Update newsfraghment --- src/towncrier/newsfragments/+0732bd4a.feature.rst | 1 - src/towncrier/newsfragments/603.feature.rst | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 src/towncrier/newsfragments/+0732bd4a.feature.rst create mode 100644 src/towncrier/newsfragments/603.feature.rst diff --git a/src/towncrier/newsfragments/+0732bd4a.feature.rst b/src/towncrier/newsfragments/+0732bd4a.feature.rst deleted file mode 100644 index bc13c913..00000000 --- a/src/towncrier/newsfragments/+0732bd4a.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The `towncrier create` action is now content aware (either interactively, or with a new `--section` option). diff --git a/src/towncrier/newsfragments/603.feature.rst b/src/towncrier/newsfragments/603.feature.rst new file mode 100644 index 00000000..32486dd7 --- /dev/null +++ b/src/towncrier/newsfragments/603.feature.rst @@ -0,0 +1,4 @@ +The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option). + +If you use sections and none have an empty path, you must now specify the section when creating a new news fragment. +If one does have an empty path, that section will be used by default. From b8b50e93431367d9ea614fca3f4e52c03f897e59 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Tue, 4 Jun 2024 16:20:26 +1200 Subject: [PATCH 05/13] No need to mention new behaviour in news fragment, since that case wouldn't have worked previously anyway --- src/towncrier/newsfragments/603.feature.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/towncrier/newsfragments/603.feature.rst b/src/towncrier/newsfragments/603.feature.rst index 32486dd7..b206076c 100644 --- a/src/towncrier/newsfragments/603.feature.rst +++ b/src/towncrier/newsfragments/603.feature.rst @@ -1,4 +1 @@ -The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option). - -If you use sections and none have an empty path, you must now specify the section when creating a new news fragment. -If one does have an empty path, that section will be used by default. +The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option). \ No newline at end of file From 836bfb43077fa9ed1a83591a7d63778be893e16f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 03:25:50 +0000 Subject: [PATCH 06/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/towncrier/newsfragments/603.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/towncrier/newsfragments/603.feature.rst b/src/towncrier/newsfragments/603.feature.rst index b206076c..afe48164 100644 --- a/src/towncrier/newsfragments/603.feature.rst +++ b/src/towncrier/newsfragments/603.feature.rst @@ -1 +1 @@ -The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option). \ No newline at end of file +The ``towncrier create`` action now uses sections defined in your config (either interactively, or via the new ``--section`` option). From 66b46179f460c0ecded88c518843c9819b8c2f31 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Mon, 10 Jun 2024 13:31:54 +1200 Subject: [PATCH 07/13] Add some test docstrings --- src/towncrier/test/test_create.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 65cbbc6d..3ec00c46 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -417,6 +417,10 @@ def test_without_filename_no_orphan_config(self, runner: CliRunner): @with_isolated_runner def test_sections(self, runner: CliRunner): + """ + When creating a new fragment, the user can specify the section from the command + line (and if non is provided, the default section will be used). + """ setup_simple_project( extra_config=""" [[tool.towncrier.section]] @@ -456,6 +460,10 @@ def test_sections(self, runner: CliRunner): @with_isolated_runner def test_sections_without_filename(self, runner: CliRunner): + """ + When multiple sections exist when the interactive prompt is used, the user is + prompted to select a section. + """ setup_simple_project( extra_config=""" [[tool.towncrier.section]] @@ -617,8 +625,7 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): Path("pyproject.toml").write_text( # Important to customize `config.directory` because the default # already supports this scenario. - "[tool.towncrier]\n" - + 'directory = "changelog.d"\n' + "[tool.towncrier]\n" + 'directory = "changelog.d"\n' ) Path("foo/foo").mkdir(parents=True) Path("foo/foo/__init__.py").write_text("") From d3361521b6f16b4c714277988a5101bac81c8229 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Tue, 11 Jun 2024 08:33:52 +1200 Subject: [PATCH 08/13] Default section --- docs/cli.rst | 2 +- src/towncrier/create.py | 65 ++++++++++++++----------------- src/towncrier/test/test_create.py | 27 ++++++------- 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 108a5429..6adbb94d 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -101,7 +101,7 @@ If that is the entire fragment name, a random hash will be added for you:: .. option:: --section SECTION The section to use for the news fragment. - Default: the first section with no path. + Default: the section with no path, or if all sections have a path then the first defined section. ``towncrier check`` diff --git a/src/towncrier/create.py b/src/towncrier/create.py index fca96c00..acb3f96c 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -105,40 +105,43 @@ def __main( if ext.lower() in (".rst", ".md"): filename_ext = ext - # Get the default section. - default_section = None - if len(config.sections) == 1: - default_section = next(iter(config.sections)) - else: - # If there are mulitple sections then the first without a path is the default - # section, otherwise there's no default. - for section_name, section_dir in config.sections.items(): - if not section_dir: - default_section = section_name - break - - if section is not None: - if section not in config.sections: - section_param = None - for p in ctx.command.params: # pragma: no branch - if p.name == "section": - section_param = p + section_provided = section is not None + if not section_provided: + # Get the default section. + if len(config.sections) == 1: + section = next(iter(config.sections)) + else: + # If there are multiple sections then the first without a path is the default + # section, otherwise it's the first defined section. + for section_name, section_dir in config.sections.items(): + if not section_dir: + section = section_name break - expected_sections = ", ".join(f"'{s}'" for s in config.sections) - raise click.BadParameter( - f"expected one of {expected_sections}", - param=section_param, - ) + if section is None: + section = list(config.sections.keys())[0] + + if section not in config.sections: + # Raise a click exception with the correct parameter. + section_param = None + for p in ctx.command.params: # pragma: no branch + if p.name == "section": + section_param = p + break + expected_sections = ", ".join(f"'{s}'" for s in config.sections) + raise click.BadParameter( + f"expected one of {expected_sections}", + param=section_param, + ) if not filename: - if section is None: + if not section_provided: sections = list(config.sections) if len(sections) > 1: click.echo("Pick a section:") default_section_index = None - for i, section in enumerate(sections): - click.echo(f" {i+1}: {section or '(primary)'}") - if not default_section_index and section == default_section: + for i, s in enumerate(sections): + click.echo(f" {i+1}: {s or '(primary)'}") + if not default_section_index and s == section: default_section_index = str(i + 1) section_index = click.prompt( "Section", @@ -182,14 +185,6 @@ def __main( if filename_parts[-1] in config.types and filename_ext: filename += filename_ext - if not section: - if default_section is None: - raise click.UsageError( - "Multiple sections defined in configuration file, all with paths." - " Please define a section with `--section`." - ) - section = default_section - get_fragments_path = FragmentsPath(base_directory, config) fragments_directory = get_fragments_path(section_directory=config.sections[section]) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 3ec00c46..79b883fe 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -419,7 +419,10 @@ def test_without_filename_no_orphan_config(self, runner: CliRunner): def test_sections(self, runner: CliRunner): """ When creating a new fragment, the user can specify the section from the command - line (and if non is provided, the default section will be used). + line (and if none is provided, the default section will be used). + + The default section is either the section with a blank path, or else the first + section defined in the configuration file. """ setup_simple_project( extra_config=""" @@ -428,21 +431,14 @@ def test_sections(self, runner: CliRunner): path = "backend" [[tool.towncrier.section]] name = "Frontend" -path = "frontend" +path = "" """ ) result = runner.invoke(_main, ["123.feature.rst"]) - self.assertTrue(result.exception, result.output) - self.assertEqual( - result.output, - """\ -Usage: create [OPTIONS] [FILENAME] -Try 'create --help' for help. - -Error: Multiple sections defined in configuration file, all with paths.\ - Please define a section with `--section`. -""", - ) + self.assertFalse(result.exception, result.output) + frag_path = Path("foo", "newsfragments") + fragments = [f.name for f in frag_path.iterdir()] + self.assertEqual(fragments, ["123.feature.rst"]) result = runner.invoke(_main, ["123.feature.rst", "--section", "invalid"]) self.assertTrue(result.exception, result.output) @@ -451,10 +447,9 @@ def test_sections(self, runner: CliRunner): result.output, ) - result = runner.invoke(_main, ["123.feature.rst", "--section", "Frontend"]) + result = runner.invoke(_main, ["123.feature.rst", "--section", "Backend"]) self.assertFalse(result.exception, result.output) - frag_path = Path("foo", "frontend", "newsfragments") - + frag_path = Path("foo", "backend", "newsfragments") fragments = [f.name for f in frag_path.iterdir()] self.assertEqual(fragments, ["123.feature.rst"]) From 3db9434674886b0b6229d3f3383c0656f7e1ca6d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:37:16 +0000 Subject: [PATCH 09/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/towncrier/test/test_create.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 79b883fe..90965266 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -620,7 +620,8 @@ def test_in_different_dir_with_nondefault_newsfragments_directory(self, runner): Path("pyproject.toml").write_text( # Important to customize `config.directory` because the default # already supports this scenario. - "[tool.towncrier]\n" + 'directory = "changelog.d"\n' + "[tool.towncrier]\n" + + 'directory = "changelog.d"\n' ) Path("foo/foo").mkdir(parents=True) Path("foo/foo/__init__.py").write_text("") From 12958092dee6e3cbeddc45789238b869f3b241cb Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Thu, 13 Jun 2024 17:25:31 +1200 Subject: [PATCH 10/13] Typing improvement --- src/towncrier/create.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/towncrier/create.py b/src/towncrier/create.py index acb3f96c..32c06d5d 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -10,6 +10,7 @@ import os from pathlib import Path +from typing import cast import click @@ -132,6 +133,7 @@ def __main( f"expected one of {expected_sections}", param=section_param, ) + section = cast(str, section) if not filename: if not section_provided: From 600631c2f26766cfeb218d979067a76fe4a0e1a4 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Thu, 13 Jun 2024 17:32:59 +1200 Subject: [PATCH 11/13] Skip an invalid branch to cover --- src/towncrier/create.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/towncrier/create.py b/src/towncrier/create.py index 32c06d5d..2fc0ef00 100644 --- a/src/towncrier/create.py +++ b/src/towncrier/create.py @@ -114,7 +114,10 @@ def __main( else: # If there are multiple sections then the first without a path is the default # section, otherwise it's the first defined section. - for section_name, section_dir in config.sections.items(): + for ( + section_name, + section_dir, + ) in config.sections.items(): # pragma: no branch if not section_dir: section = section_name break From 3c4d9f9f0b6205565bf4974299b3ef33f5465588 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Thu, 13 Jun 2024 17:40:01 +1200 Subject: [PATCH 12/13] Add test for multiple sections all with paths --- src/towncrier/test/test_create.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/towncrier/test/test_create.py b/src/towncrier/test/test_create.py index 90965266..dc6f6b9d 100644 --- a/src/towncrier/test/test_create.py +++ b/src/towncrier/test/test_create.py @@ -494,6 +494,10 @@ def test_sections_without_filename(self, runner: CliRunner): @with_isolated_runner def test_sections_without_filename_with_section_option(self, runner: CliRunner): + """ + When multiple sections exist and the section is provided via the command line, + the user isn't prompted to select a section. + """ setup_simple_project( extra_config=""" [[tool.towncrier.section]] @@ -525,6 +529,28 @@ def test_sections_without_filename_with_section_option(self, runner: CliRunner): """, ) + @with_isolated_runner + def test_sections_all_with_paths(self, runner: CliRunner): + """ + When all sections have paths, the first is the default. + """ + setup_simple_project( + extra_config=""" +[[tool.towncrier.section]] +name = "Frontend" +path = "frontend" + +[[tool.towncrier.section]] +name = "Backend" +path = "backend" +""" + ) + result = runner.invoke(_main, ["123.feature.rst"]) + self.assertFalse(result.exception, result.output) + frag_path = Path("foo", "frontend", "newsfragments") + fragments = [f.name for f in frag_path.iterdir()] + self.assertEqual(fragments, ["123.feature.rst"]) + @with_isolated_runner def test_without_filename_with_message(self, runner: CliRunner): """ From 10b71643419643194e32acc6ffd51ca3fa05e645 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Fri, 14 Jun 2024 10:46:47 +1200 Subject: [PATCH 13/13] Fix merge --- src/towncrier/_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index b571a490..bfb05227 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -126,7 +126,7 @@ def find_fragments( for basename in files: issue, category, counter = parse_newfragment_basename( - basename, frag_type_names + basename, config.types ) if category is None: continue