From d05e42068b4823dc781b334c548e90c592d741fe Mon Sep 17 00:00:00 2001 From: jakkdl Date: Wed, 17 Jul 2024 17:06:14 +0200 Subject: [PATCH 1/6] quick implementation of fix for issue numbers being appended to a code block --- src/towncrier/_builder.py | 12 ++++ src/towncrier/test/test_format.py | 97 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index e7fbf12b..89da4b2c 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -305,6 +305,17 @@ def render_fragments( data: dict[str, dict[str, dict[str, list[str]]]] = {} issues_by_category: dict[str, dict[str, list[str]]] = {} + def maybe_append_newlines(text: str) -> str: + # if a newsfragment text ends with a code block, we want to append two newlines + # so issue number(s) don't get inserted into the block + if re.search(r"::\n\n([ \t]*\S*)*$", text): + # We insert one space, the default template inserts another, which results + # in the correct indentation given default bullet indentation. + # TODO: This breaks on different indentation though, and would require instead + # doing this change after the template has been applied.... I thought? + return text + "\n\n " + return text + for section_name, section_value in fragments.items(): data[section_name] = {} issues_by_category[section_name] = {} @@ -337,6 +348,7 @@ def render_fragments( # for the template, after formatting each issue number categories = {} for text, issues in entries: + text = maybe_append_newlines(text) rendered = [render_issue(issue_format, i) for i in issues] categories[text] = rendered diff --git a/src/towncrier/test/test_format.py b/src/towncrier/test/test_format.py index a1efefb5..46a75db1 100644 --- a/src/towncrier/test/test_format.py +++ b/src/towncrier/test/test_format.py @@ -462,3 +462,100 @@ def test_line_wrapping_disabled(self): versiondata={"name": "MyProject", "version": "1.0", "date": "never"}, ) self.assertEqual(output, expected_output) + + def test_trailing_block(self): + """ + Make sure a newline gets inserted before appending the issue number, if the + newsfragment ends with an indented block. + """ + + fragments = { + "": { + ( + "1", + "feature", + 0, + ): "text::\n\n def foo(): ..." + } + } + expected_output = """MyProject 1.0 (never) +===================== + +Features +-------- + +- text:: + + def foo(): ... + + (#1) + + +""" + + # TODO: I copy-pasted the below lines from previous test, they probably contain + # crap that's irrelevant to this test. + definitions = { + "feature": {"name": "Features", "showcontent": True}, + } + template = read_pkg_resource("templates/default.rst") + + fragments = split_fragments(fragments, definitions) + output = render_fragments( + template, + None, + fragments, + definitions, + ["-", "~"], + wrap=True, + versiondata={"name": "MyProject", "version": "1.0", "date": "never"}, + ) + self.assertEqual(output, expected_output) + + def test_trailing_block_nondefault_bullets(self): + """ + Test insertion of newlines after code block with single-file-no-bullets. + """ + # TODO: I expected this to break with the issue number being indented, but + # instead I got an extra newline? Which seems fine? + + fragments = { + "": { + ( + "1", + "feature", + 0, + ): "text::\n\n def foo(): ..." + } + } + expected_output = """MyProject 1.0 (never) +===================== + +Features +-------- + +text:: + + def foo(): ... + + +(#1) + +""" + + definitions = { + "feature": {"name": "Features", "showcontent": True}, + } + template = read_pkg_resource("templates/single-file-no-bullets.rst") + + fragments = split_fragments(fragments, definitions) + output = render_fragments( + template, + None, + fragments, + definitions, + ["-", "~"], + wrap=True, + versiondata={"name": "MyProject", "version": "1.0", "date": "never"}, + ) + self.assertEqual(output, expected_output) From b8454dab8832aa95a917b2f84b62cc29e1fcc196 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Sat, 31 Aug 2024 13:18:28 +0200 Subject: [PATCH 2/6] add newsfragment --- src/towncrier/newsfragments/614.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/towncrier/newsfragments/614.bugfix.rst diff --git a/src/towncrier/newsfragments/614.bugfix.rst b/src/towncrier/newsfragments/614.bugfix.rst new file mode 100644 index 00000000..f8b12a6e --- /dev/null +++ b/src/towncrier/newsfragments/614.bugfix.rst @@ -0,0 +1 @@ +Multi-line newsfragments that ends with a code block will now have a newline inserted before appending the link to the issue, to avoid breaking formatting. From 63c4c430330d5330bef54bd26e48dcb3568a5063 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Mon, 2 Sep 2024 15:46:50 +0200 Subject: [PATCH 3/6] fix, clean up, and move helper function outside of render_fragments. Cleaned up and expanded test. Removed single-file-no-bullets test --- src/towncrier/_builder.py | 36 ++++++++++------ src/towncrier/test/test_format.py | 70 ++++++++----------------------- 2 files changed, 41 insertions(+), 65 deletions(-) diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index 895c6a0b..da6e090b 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -328,6 +328,29 @@ def render_issue(issue_format: str | None, issue: str) -> str: return issue_format.format(issue=issue) +def append_newlines_if_trailing_code_block(text: str) -> str: + """ + Appends two newlines to a text string if it ends with a code block. + + Used by `render_fragments` to avoid appending link to issue number into the code block. + """ + # Search for the existence of a code block at the end. We do this by searching for: + # 1. start of code block: two ":", followed by two newlines + # 2. any number of indented, or empty, lines (or the code block would end) + # 3. one line of indented text w/o a trailing newline (because the string is stripped) + # 4. end of the string. + indented_text=r" [ \t]+[^\n]*" + empty_or_indented_text_lines=f"(({indented_text})?\n)*" + regex = r"::\n\n" + empty_or_indented_text_lines + indented_text + "$" + if re.search(regex, text): + # We insert one space, the default template inserts another, which results + # in the correct indentation given default bullet indentation. + # Non-default templates with different indentation will likely encounter issues + # if they have trailing code blocks. + return text + "\n\n " + return text + + def render_fragments( template: str, issue_format: str | None, @@ -349,17 +372,6 @@ def render_fragments( data: dict[str, dict[str, dict[str, list[str]]]] = {} issues_by_category: dict[str, dict[str, list[str]]] = {} - def maybe_append_newlines(text: str) -> str: - # if a newsfragment text ends with a code block, we want to append two newlines - # so issue number(s) don't get inserted into the block - if re.search(r"::\n\n([ \t]*\S*)*$", text): - # We insert one space, the default template inserts another, which results - # in the correct indentation given default bullet indentation. - # TODO: This breaks on different indentation though, and would require instead - # doing this change after the template has been applied.... I thought? - return text + "\n\n " - return text - for section_name, section_value in fragments.items(): data[section_name] = {} issues_by_category[section_name] = {} @@ -392,7 +404,7 @@ def maybe_append_newlines(text: str) -> str: # for the template, after formatting each issue number categories = {} for text, issues in entries: - text = maybe_append_newlines(text) + text = append_newlines_if_trailing_code_block(text) rendered = [render_issue(issue_format, i) for i in issues] categories[text] = rendered diff --git a/src/towncrier/test/test_format.py b/src/towncrier/test/test_format.py index 46a75db1..9af21456 100644 --- a/src/towncrier/test/test_format.py +++ b/src/towncrier/test/test_format.py @@ -463,7 +463,7 @@ def test_line_wrapping_disabled(self): ) self.assertEqual(output, expected_output) - def test_trailing_block(self): + def test_trailing_block(self) -> None: """ Make sure a newline gets inserted before appending the issue number, if the newsfragment ends with an indented block. @@ -475,84 +475,48 @@ def test_trailing_block(self): "1", "feature", 0, - ): "text::\n\n def foo(): ..." + ): "this fragment has a trailing code block::\n\n def foo(): ...\n\n \n def bar(): ...", + ( + "2", + "feature", + 0, + ): "this block is not trailing::\n\n def foo(): ...\n def bar(): ...\n\nso we can append the issue number directly after this", } } + # the line with 3 spaces (and nothing else) is stripped expected_output = """MyProject 1.0 (never) ===================== Features -------- -- text:: +- this fragment has a trailing code block:: def foo(): ... - (#1) - - -""" - - # TODO: I copy-pasted the below lines from previous test, they probably contain - # crap that's irrelevant to this test. - definitions = { - "feature": {"name": "Features", "showcontent": True}, - } - template = read_pkg_resource("templates/default.rst") - fragments = split_fragments(fragments, definitions) - output = render_fragments( - template, - None, - fragments, - definitions, - ["-", "~"], - wrap=True, - versiondata={"name": "MyProject", "version": "1.0", "date": "never"}, - ) - self.assertEqual(output, expected_output) + def bar(): ... - def test_trailing_block_nondefault_bullets(self): - """ - Test insertion of newlines after code block with single-file-no-bullets. - """ - # TODO: I expected this to break with the issue number being indented, but - # instead I got an extra newline? Which seems fine? - - fragments = { - "": { - ( - "1", - "feature", - 0, - ): "text::\n\n def foo(): ..." - } - } - expected_output = """MyProject 1.0 (never) -===================== - -Features --------- - -text:: + (#1) +- this block is not trailing:: def foo(): ... + def bar(): ... + so we can append the issue number directly after this (#2) -(#1) """ definitions = { "feature": {"name": "Features", "showcontent": True}, } - template = read_pkg_resource("templates/single-file-no-bullets.rst") - - fragments = split_fragments(fragments, definitions) + template = read_pkg_resource("templates/default.rst") + fragments_split = split_fragments(fragments, definitions) output = render_fragments( template, None, - fragments, + fragments_split, definitions, ["-", "~"], wrap=True, From a8b39319af88538c7f32812edcfe00056091b97c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:47:41 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/towncrier/_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/towncrier/_builder.py b/src/towncrier/_builder.py index da6e090b..33ebe167 100644 --- a/src/towncrier/_builder.py +++ b/src/towncrier/_builder.py @@ -339,8 +339,8 @@ def append_newlines_if_trailing_code_block(text: str) -> str: # 2. any number of indented, or empty, lines (or the code block would end) # 3. one line of indented text w/o a trailing newline (because the string is stripped) # 4. end of the string. - indented_text=r" [ \t]+[^\n]*" - empty_or_indented_text_lines=f"(({indented_text})?\n)*" + indented_text = r" [ \t]+[^\n]*" + empty_or_indented_text_lines = f"(({indented_text})?\n)*" regex = r"::\n\n" + empty_or_indented_text_lines + indented_text + "$" if re.search(regex, text): # We insert one space, the default template inserts another, which results From 4a302958686619d57304cd19599d39d4fded6064 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Mon, 2 Sep 2024 15:52:30 +0200 Subject: [PATCH 5/6] fix pre-commit --- src/towncrier/test/test_format.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/towncrier/test/test_format.py b/src/towncrier/test/test_format.py index 9af21456..1a711a4a 100644 --- a/src/towncrier/test/test_format.py +++ b/src/towncrier/test/test_format.py @@ -475,12 +475,19 @@ def test_trailing_block(self) -> None: "1", "feature", 0, - ): "this fragment has a trailing code block::\n\n def foo(): ...\n\n \n def bar(): ...", + ): ( + "this fragment has a trailing code block::\n\n" + "def foo(): ...\n\n \n def bar(): ..." + ), ( "2", "feature", 0, - ): "this block is not trailing::\n\n def foo(): ...\n def bar(): ...\n\nso we can append the issue number directly after this", + ): ( + "this block is not trailing::\n\n" + "def foo(): ...\n def bar(): ..." + "\n\nso we can append the issue number directly after this" + ), } } # the line with 3 spaces (and nothing else) is stripped From fb995e1f4b2a53722a4c975bae4be5599e147b15 Mon Sep 17 00:00:00 2001 From: jakkdl Date: Tue, 19 Nov 2024 16:42:46 +0100 Subject: [PATCH 6/6] fix tests after 4a30295 deleted some spaces --- src/towncrier/test/test_format.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/towncrier/test/test_format.py b/src/towncrier/test/test_format.py index 1a711a4a..7be2877e 100644 --- a/src/towncrier/test/test_format.py +++ b/src/towncrier/test/test_format.py @@ -477,7 +477,9 @@ def test_trailing_block(self) -> None: 0, ): ( "this fragment has a trailing code block::\n\n" - "def foo(): ...\n\n \n def bar(): ..." + " def foo(): ...\n\n" + " \n" + " def bar(): ..." ), ( "2", @@ -485,8 +487,9 @@ def test_trailing_block(self) -> None: 0, ): ( "this block is not trailing::\n\n" - "def foo(): ...\n def bar(): ..." - "\n\nso we can append the issue number directly after this" + " def foo(): ...\n" + " def bar(): ...\n\n" + "so we can append the issue number directly after this" ), } }