diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 2795c511..79465696 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -22,6 +22,7 @@ import logging import os import sys +import re from release_notes_generator.utils.constants import ( GITHUB_REPOSITORY, @@ -41,10 +42,10 @@ SKIP_RELEASE_NOTES_LABELS, RELEASE_NOTES_TITLE, RELEASE_NOTE_TITLE_DEFAULT, + SUPPORTED_ROW_FORMAT_KEYS, ) from release_notes_generator.utils.enums import DuplicityScopeEnum from release_notes_generator.utils.gh_action import get_action_input -from release_notes_generator.utils.utils import detect_row_format_invalid_keywords logger = logging.getLogger(__name__) @@ -55,6 +56,10 @@ class ActionInputs: A class representing the inputs provided to the GH action. """ + _row_format_issue = None + _row_format_pr = None + _row_format_link_pr = None + @staticmethod def get_github_repository() -> str: """ @@ -171,14 +176,22 @@ def get_row_format_issue() -> str: """ Get the issue row format for the release notes. """ - return get_action_input(ROW_FORMAT_ISSUE, "{number} _{title}_ in {pull-requests}").strip() + if ActionInputs._row_format_issue is None: + ActionInputs._row_format_issue = ActionInputs._detect_row_format_invalid_keywords( + get_action_input(ROW_FORMAT_ISSUE, "{number} _{title}_ in {pull-requests}").strip(), clean=True + ) + return ActionInputs._row_format_issue @staticmethod def get_row_format_pr() -> str: """ Get the pr row format for the release notes. """ - return get_action_input(ROW_FORMAT_PR, "{number} _{title}_").strip() + if ActionInputs._row_format_pr is None: + ActionInputs._row_format_pr = ActionInputs._detect_row_format_invalid_keywords( + get_action_input(ROW_FORMAT_PR, "{number} _{title}_").strip(), clean=True + ) + return ActionInputs._row_format_pr @staticmethod def get_row_format_link_pr() -> bool: @@ -236,13 +249,13 @@ def validate_inputs() -> None: if not isinstance(row_format_issue, str) or not row_format_issue.strip(): errors.append("Issue row format must be a non-empty string.") - errors.extend(detect_row_format_invalid_keywords(row_format_issue)) + ActionInputs._detect_row_format_invalid_keywords(row_format_issue) row_format_pr = ActionInputs.get_row_format_pr() if not isinstance(row_format_pr, str) or not row_format_pr.strip(): errors.append("PR Row format must be a non-empty string.") - errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR")) + ActionInputs._detect_row_format_invalid_keywords(row_format_pr, row_type="PR") row_format_link_pr = ActionInputs.get_row_format_link_pr() ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors) @@ -266,3 +279,26 @@ def validate_inputs() -> None: logger.debug("Warnings: %s", warnings) logger.debug("Print empty chapters: %s", print_empty_chapters) logger.debug("Release notes title: %s", release_notes_title) + + @staticmethod + def _detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue", clean: bool = False) -> str: + """ + Detects invalid keywords in the row format. + + @param row_format: The row format to be checked for invalid keywords. + @param row_type: The type of row format. Default is "Issue". + @return: If clean is True, the cleaned row format. Otherwise, the original row format. + """ + keywords_in_braces = re.findall(r"\{(.*?)\}", row_format) + invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS] + cleaned_row_format = row_format + for invalid_keyword in invalid_keywords: + logger.error( + "Invalid `{}` detected in `{}` row format keyword(s) found: {}. Will be removed from string.".format( + invalid_keyword, row_type, ", ".join(invalid_keywords) + ) + ) + if clean: + cleaned_row_format = cleaned_row_format.replace(f"{{{invalid_keyword}}}", "") + + return cleaned_row_format diff --git a/release_notes_generator/utils/utils.py b/release_notes_generator/utils/utils.py index f1a38695..ee7be1b0 100644 --- a/release_notes_generator/utils/utils.py +++ b/release_notes_generator/utils/utils.py @@ -19,15 +19,12 @@ """ import logging -import re from typing import Optional from github.GitRelease import GitRelease from github.Repository import Repository -from release_notes_generator.utils.constants import SUPPORTED_ROW_FORMAT_KEYS - logger = logging.getLogger(__name__) @@ -57,19 +54,3 @@ def get_change_url( changelog_url = f"https://github.com/{repo.full_name}/compare/{rls.tag_name}...{tag_name}" return changelog_url - - -def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") -> list[str]: - """ - Detects invalid keywords in the row format. - - @param row_format: The row format to be checked for invalid keywords. - @param row_type: The type of row format. Default is "Issue". - @return: A list of errors if invalid keywords are found, otherwise an empty list. - """ - errors = [] - keywords_in_braces = re.findall(r"\{(.*?)\}", row_format) - invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS] - if invalid_keywords: - errors.append(f"Invalid {row_type} row format keyword(s) found: {', '.join(invalid_keywords)}") - return errors diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py index 493afa78..2afc31b0 100644 --- a/tests/test_action_inputs.py +++ b/tests/test_action_inputs.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import logging import pytest @@ -43,6 +44,8 @@ ("get_verbose", "not_bool", "Verbose logging must be a boolean."), ("get_duplicity_icon", "", "Duplicity icon must be a non-empty string and have a length of 1."), ("get_duplicity_icon", "Oj", "Duplicity icon must be a non-empty string and have a length of 1."), + ("get_row_format_issue", "", "Issue row format must be a non-empty string."), + ("get_row_format_pr", "", "PR Row format must be a non-empty string."), ] @@ -163,3 +166,37 @@ def test_get_duplicity_scope_wrong_value(mocker): assert ActionInputs.get_duplicity_scope() == "BOTH" mock_error.assert_called_with("Error: '%s' is not a valid DuplicityType.", "HUH") + + +def test_detect_row_format_invalid_keywords_no_invalid_keywords(caplog): + caplog.set_level(logging.ERROR) + row_format = "{number} _{title}_ in {pull-requests}" + ActionInputs._detect_row_format_invalid_keywords(row_format) + assert len(caplog.records) == 0 + + +def test_detect_row_format_invalid_keywords_with_invalid_keywords(caplog): + caplog.set_level(logging.ERROR) + row_format = "{number} _{title}_ in {pull-requests} {invalid_key} {another_invalid}" + ActionInputs._detect_row_format_invalid_keywords(row_format) + assert len(caplog.records) == 2 + expected_errors = [ + "Invalid `invalid_key` detected in `Issue` row format keyword(s) found: invalid_key, another_invalid. Will be removed from string.", + "Invalid `another_invalid` detected in `Issue` row format keyword(s) found: invalid_key, another_invalid. Will be removed from string." + ] + actual_errors = [record.getMessage() for record in caplog.records] + assert actual_errors == expected_errors + + +def test_clean_row_format_invalid_keywords_no_keywords(): + expected_row_format = "{number} _{title}_ in {pull-requests}" + actual_format = ActionInputs._detect_row_format_invalid_keywords(expected_row_format, clean=True) + assert expected_row_format == actual_format + + +def test_clean_row_format_invalid_keywords_nested_braces(): + row_format = "{number} _{title}_ in {pull-requests} {invalid_key} {another_invalid}" + expected_format = "{number} _{title}_ in {pull-requests} " + actual_format = ActionInputs._detect_row_format_invalid_keywords(row_format, clean=True) + assert expected_format == actual_format + diff --git a/tests/test_release_notes_generator.py b/tests/test_release_notes_generator.py index d0276865..75ef50c6 100644 --- a/tests/test_release_notes_generator.py +++ b/tests/test_release_notes_generator.py @@ -21,6 +21,7 @@ from release_notes_generator.generator import ReleaseNotesGenerator from release_notes_generator.model.custom_chapters import CustomChapters +from release_notes_generator.utils.constants import ROW_FORMAT_ISSUE # generate_release_notes tests @@ -111,6 +112,11 @@ def test_generate_release_notes_latest_release_found_by_created_at( mock_rate_limit.core.remaining = 1000 github_mock.get_rate_limit.return_value = mock_rate_limit + mock_get_action_input = mocker.patch("release_notes_generator.utils.gh_action.get_action_input") + mock_get_action_input.side_effect = lambda first_arg, **kwargs: ( + "{number} _{title}_ in {pull-requests} {unknown} {another-unknown}" if first_arg == ROW_FORMAT_ISSUE else None + ) + custom_chapters = CustomChapters(print_empty_chapters=True) release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index c7d3f15f..a02fd3d4 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -14,7 +14,7 @@ # limitations under the License. # -from release_notes_generator.utils.utils import get_change_url, detect_row_format_invalid_keywords +from release_notes_generator.utils.utils import get_change_url # get_change_url @@ -33,19 +33,3 @@ def test_get_change_url_no_git_release(mock_repo): def test_get_change_url_with_git_release(mock_repo, mock_git_release): url = get_change_url(tag_name="v2.0.0", repository=mock_repo, git_release=mock_git_release) assert url == "https://github.com/org/repo/compare/v1.0.0...v2.0.0" - - -# detect_row_format_invalid_keywords - - -def test_valid_row_format(): - row_format = "{number} - {title} in {pull-requests}" - errors = detect_row_format_invalid_keywords(row_format) - assert not errors, "Expected no errors for valid keywords" - - -def test_multiple_invalid_keywords(): - row_format = "{number} - {link} - {Title} and {Pull-requests}" - errors = detect_row_format_invalid_keywords(row_format) - assert len(errors) == 1 - assert "Invalid Issue row format keyword(s) found: link, Title, Pull-requests" in errors[0]