From f5a4e59f46466987ce3dccd5dc3cd086b0ddc449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Forr=C3=B3?= Date: Thu, 26 Sep 2024 14:14:57 +0200 Subject: [PATCH 1/2] Move `count_brackets()` to `specfile.utils` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nikola Forró --- specfile/macro_definitions.py | 31 +--------------------------- specfile/utils.py | 39 +++++++++++++++++++++++++++++++++++ tests/unit/test_utils.py | 19 ++++++++++++++++- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/specfile/macro_definitions.py b/specfile/macro_definitions.py index 2b53c9c..cc7c775 100644 --- a/specfile/macro_definitions.py +++ b/specfile/macro_definitions.py @@ -9,7 +9,7 @@ from specfile.conditions import process_conditions from specfile.formatter import formatted from specfile.types import SupportsIndex -from specfile.utils import UserList +from specfile.utils import UserList, count_brackets if TYPE_CHECKING: from specfile.specfile import Specfile @@ -303,35 +303,6 @@ def pop(lines): else: return line - def count_brackets(s): - bc = pc = 0 - chars = list(s) - while chars: - c = chars.pop(0) - if c == "\\" and chars: - chars.pop(0) - continue - if c == "%" and chars: - c = chars.pop(0) - if c == "{": - bc += 1 - elif c == "(": - pc += 1 - continue - if c == "{" and bc > 0: - bc += 1 - continue - if c == "}" and bc > 0: - bc -= 1 - continue - if c == "(" and pc > 0: - pc += 1 - continue - if c == ")" and pc > 0: - pc -= 1 - continue - return bc, pc - md_regex = re.compile( r""" ^ diff --git a/specfile/utils.py b/specfile/utils.py index 545a75b..a628a0f 100644 --- a/specfile/utils.py +++ b/specfile/utils.py @@ -254,6 +254,45 @@ def get_filename_from_location(location: str) -> str: return location[slash + 1 :].split("=")[-1] +def count_brackets(string: str) -> Tuple[int, int]: + """ + Counts non-pair brackets in %{...} and %(...) expressions appearing in the given string. + + Args: + string: Input string. + + Returns: + The count of non-pair curly braces and the count of non-pair parentheses. + """ + bc = pc = 0 + chars = list(string) + while chars: + c = chars.pop(0) + if c == "\\" and chars: + chars.pop(0) + continue + if c == "%" and chars: + c = chars.pop(0) + if c == "{": + bc += 1 + elif c == "(": + pc += 1 + continue + if c == "{" and bc > 0: + bc += 1 + continue + if c == "}" and bc > 0: + bc -= 1 + continue + if c == "(" and pc > 0: + pc += 1 + continue + if c == ")" and pc > 0: + pc -= 1 + continue + return bc, pc + + def split_conditional_macro_expansion(value: str) -> Tuple[str, str, str]: """ Splits conditional macro expansion into its body and prefix and suffix of it. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ed79278..8f90d04 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -3,7 +3,7 @@ import pytest -from specfile.utils import EVR, NEVR, NEVRA, get_filename_from_location +from specfile.utils import EVR, NEVR, NEVRA, count_brackets, get_filename_from_location @pytest.mark.parametrize( @@ -32,6 +32,23 @@ def test_get_filename_from_location(location, filename): assert get_filename_from_location(location) == filename +@pytest.mark.parametrize( + "string, count", + [ + ("", (0, 0)), + ("%macro", (0, 0)), + ("%{macro}", (0, 0)), + ("%{{macro}}", (0, 0)), + ("%{{macro}", (1, 0)), + ("%{macro:", (1, 0)), + ("%(echo %{v}", (0, 1)), + ("%(echo %{v} | cut -d. -f3)", (0, 0)), + ], +) +def test_count_brackets(string, count): + assert count_brackets(string) == count + + def test_EVR_compare(): assert EVR(version="0") == EVR(version="0") assert EVR(version="0", release="1") != EVR(version="0", release="2") From b20d07208cd57c02d40e81a4a03c340ebb20025f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Forr=C3=B3?= Date: Thu, 26 Sep 2024 14:16:19 +0200 Subject: [PATCH 2/2] Handle multi-line tag values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nikola Forró --- specfile/tags.py | 31 ++++++++++++++++++++++++++----- tests/unit/test_tags.py | 28 +++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/specfile/tags.py b/specfile/tags.py index ea8a469..f89c4a0 100644 --- a/specfile/tags.py +++ b/specfile/tags.py @@ -23,7 +23,7 @@ from specfile.macros import Macros from specfile.sections import Section from specfile.types import SupportsIndex -from specfile.utils import UserList, split_conditional_macro_expansion +from specfile.utils import UserList, count_brackets, split_conditional_macro_expansion if TYPE_CHECKING: from specfile.specfile import Specfile @@ -489,6 +489,13 @@ def parse(cls, section: Section, context: Optional["Specfile"] = None) -> "Tags" New instance of `Tags` class. """ + def pop(lines): + line = lines.pop(0) + if isinstance(line, str): + return line, True + else: + return line + def regex_pattern(tag): name_regex = get_tag_name_regex(tag) return rf"^(?P{name_regex})(?P\s*:\s*)(?P.+)" @@ -498,7 +505,8 @@ def regex_pattern(tag): tag_regexes = [re.compile(regex_pattern(t), re.IGNORECASE) for t in TAG_NAMES] data = [] buffer: List[str] = [] - for line, valid in lines: + while lines: + line, valid = pop(lines) ws = "" tokens = re.split(r"([^\S\n]+)$", line, maxsplit=1) if len(tokens) > 1: @@ -507,10 +515,21 @@ def regex_pattern(tag): # find out if there is a match for one of the tag regexes m = next((m for m in (r.match(line) for r in tag_regexes) if m), None) if m: + value = m.group("v") + if not suffix: + bc, pc = count_brackets(value) + while (bc > 0 or pc > 0) and lines: + value += ws + line, _ = pop(lines) + tokens = re.split(r"([^\S\n]+)$", line, maxsplit=1) + if len(tokens) > 1: + line, ws, _ = tokens + value += "\n" + line + bc, pc = count_brackets(value) data.append( Tag( m.group("n"), - m.group("v"), + value, m.group("s"), Comments.parse(buffer), valid, @@ -534,8 +553,10 @@ def get_raw_section_data(self) -> List[str]: result = [] for tag in self.data: result.extend(tag.comments.get_raw_data()) - result.append( - f"{tag._prefix}{tag.name}{tag._separator}{tag.value}{tag._suffix}" + result.extend( + f"{tag._prefix}{tag.name}{tag._separator}{tag.value}{tag._suffix}".split( + "\n" + ) ) result.extend(self._remainder) return result diff --git a/tests/unit/test_tags.py b/tests/unit/test_tags.py index 7d4218f..b30ec4d 100644 --- a/tests/unit/test_tags.py +++ b/tests/unit/test_tags.py @@ -43,6 +43,11 @@ def test_parse(): "Epoch: 1", "%endif", "", + "License: %{shrink:", + " MIT AND", + " (MIT OR Apache-2.0)", + " }", + "", "Requires: make ", "Requires(post): bash", "", @@ -62,6 +67,13 @@ def test_parse(): assert not tags[1].comments assert tags.release.comments[0].prefix == " # " assert tags.epoch.name == "Epoch" + assert tags[-6].name == "License" + assert ( + tags[-6].value == "%{shrink:\n" + " MIT AND\n" + " (MIT OR Apache-2.0)\n" + " }" + ) assert tags.requires.value == "make" assert "requires(post)" in tags assert tags[-4].name == "Requires(post)" @@ -102,11 +114,20 @@ def test_get_raw_section_data(): Comments([Comment("this is a valid comment", " # ")]), ), Tag("Epoch", "1", ": ", Comments([], ["", "%if 0"])), + Tag( + "License", + "%{shrink:\n" + " MIT AND\n" + " (MIT OR Apache-2.0)\n" + " }", + ": ", + Comments([], ["%endif", ""]), + ), Tag( "Requires", "make", ": ", - Comments([], ["%endif", ""]), + Comments([], [""]), True, "", " ", @@ -141,6 +162,11 @@ def test_get_raw_section_data(): "Epoch: 1", "%endif", "", + "License: %{shrink:", + " MIT AND", + " (MIT OR Apache-2.0)", + " }", + "", "Requires: make ", "Requires(post): bash", "",