diff --git a/fedora/python-specfile.spec b/fedora/python-specfile.spec index 4d8dbb6..48c2556 100644 --- a/fedora/python-specfile.spec +++ b/fedora/python-specfile.spec @@ -58,7 +58,7 @@ sed -i 's/rpm-py-installer/rpm/' setup.cfg %pyproject_save_files specfile -%if 0%{?with_tests} +%if %{with tests} %check %pytest %endif diff --git a/specfile/constants.py b/specfile/constants.py new file mode 100644 index 0000000..f402422 --- /dev/null +++ b/specfile/constants.py @@ -0,0 +1,193 @@ +# Copyright Contributors to the Packit project. +# SPDX-License-Identifier: MIT + +# valid section names as defined in build/parseSpec.c in RPM source +SECTION_NAMES = { + "package", + "prep", + "generate_buildrequires", + "conf", + "build", + "install", + "check", + "clean", + "preun", + "postun", + "pretrans", + "posttrans", + "pre", + "post", + "files", + "changelog", + "description", + "triggerpostun", + "triggerprein", + "triggerun", + "triggerin", + "trigger", + "verifyscript", + "sepolicy", + "filetriggerin", + "filetrigger", + "filetriggerun", + "filetriggerpostun", + "transfiletriggerin", + "transfiletrigger", + "transfiletriggerun", + "transfiletriggerpostun", + "end", + "patchlist", + "sourcelist", +} + +# valid tag names as defined in build/parsePreamble.c in RPM source +TAG_NAMES = { + "name", + "version", + "release", + "epoch", + "summary", + "license", + "distribution", + "disturl", + "vendor", + "group", + "packager", + "url", + "vcs", + "source", + "patch", + "nosource", + "nopatch", + "excludearch", + "exclusivearch", + "excludeos", + "exclusiveos", + "icon", + "provides", + "requires", + "recommends", + "suggests", + "supplements", + "enhances", + "prereq", + "conflicts", + "obsoletes", + "prefixes", + "prefix", + "buildroot", + "buildarchitectures", + "buildarch", + "buildconflicts", + "buildprereq", + "buildrequires", + "autoreqprov", + "autoreq", + "autoprov", + "docdir", + "disttag", + "bugurl", + "translationurl", + "upstreamreleases", + "orderwithrequires", + "removepathpostfixes", + "modularitylabel", +} + +# tags that can optionally have an argument (language or qualifier) +TAGS_WITH_ARG = { + "summary", + "group", + "requires", + "prereq", + "orderwithrequires", +} + +# canonical architecture names as defined in rpmrc.in in RPM source +ARCH_NAMES = { + "aarch64", + "alpha", + "alphaev5", + "alphaev56", + "alphaev6", + "alphaev67", + "alphapca56", + "amd64", + "armv3l", + "armv4b", + "armv4l", + "armv5tejl", + "armv5tel", + "armv5tl", + "armv6hl", + "armv6l", + "armv7hl", + "armv7hnl", + "armv7l", + "armv8hl", + "armv8l", + "atariclone", + "atarist", + "atariste", + "ataritt", + "athlon", + "em64t", + "falcon", + "geode", + "hades", + "i370", + "i386", + "i486", + "i586", + "i686", + "ia32e", + "ia64", + "IP", + "loongarch64", + "m68k", + "m68kmint", + "milan", + "mips", + "mips64", + "mips64el", + "mips64r6", + "mips64r6el", + "mipsel", + "mipsr6", + "mipsr6el", + "pentium3", + "pentium4", + "ppc", + "ppc32dy4", + "ppc64", + "ppc64iseries", + "ppc64le", + "ppc64p7", + "ppc64pseries", + "ppc8260", + "ppc8560", + "ppciseries", + "ppcpseries", + "riscv", + "riscv64", + "rs6000", + "s390", + "s390x", + "sh", + "sh3", + "sh4", + "sh4a", + "sparc", + "sparc64", + "sparc64v", + "sparcv8", + "sparcv9", + "sparcv9v", + "sun4", + "sun4c", + "sun4d", + "sun4m", + "sun4u", + "x86_64", + "xtensa", +} diff --git a/specfile/sections.py b/specfile/sections.py index 2ef575a..7ba72bd 100644 --- a/specfile/sections.py +++ b/specfile/sections.py @@ -5,45 +5,7 @@ import re from typing import List, Optional, SupportsIndex, Union, cast, overload -# valid section names as defined in build/parseSpec.c in RPM source -SECTION_NAMES = { - "package", - "prep", - "generate_buildrequires", - "conf", - "build", - "install", - "check", - "clean", - "preun", - "postun", - "pretrans", - "posttrans", - "pre", - "post", - "files", - "changelog", - "description", - "triggerpostun", - "triggerprein", - "triggerun", - "triggerin", - "trigger", - "verifyscript", - "sepolicy", - "filetriggerin", - "filetrigger", - "filetriggerun", - "filetriggerpostun", - "transfiletriggerin", - "transfiletrigger", - "transfiletriggerun", - "transfiletriggerpostun", - "end", - "patchlist", - "sourcelist", -} - +from specfile.constants import SECTION_NAMES # name for the implicit "preamble" section PREAMBLE = "package" @@ -93,6 +55,11 @@ def __getitem__(self, i): else: return self.data[i] + @property + def normalized_name(self) -> str: + """Normalized name of the section. All characters are lowercased.""" + return self.name.lower() + def copy(self) -> "Section": return Section(self.name, self.data) diff --git a/specfile/specfile.py b/specfile/specfile.py index 9ca13d4..0b60d5c 100644 --- a/specfile/specfile.py +++ b/specfile/specfile.py @@ -10,10 +10,12 @@ from pathlib import Path from typing import Iterator, List, Optional, Tuple, Type, Union +import rpm + from specfile.changelog import Changelog, ChangelogEntry from specfile.exceptions import SourceNumberException, SpecfileException from specfile.macro_definitions import MacroDefinition, MacroDefinitions -from specfile.macros import Macros +from specfile.macros import Macro, Macros from specfile.prep import Prep from specfile.sections import Section, Sections from specfile.sourcelist import Sourcelist @@ -103,6 +105,11 @@ def tainted(self) -> bool: """ return self._parser.tainted + @property + def rpm_spec(self) -> rpm.spec: + """Underlying `rpm.spec` instance.""" + return self._parser.spec + def reload(self) -> None: """Reload the spec file content.""" self._lines = self.path.read_text().splitlines() @@ -128,6 +135,19 @@ def expand( self._parser.parse(str(self), extra_macros) return Macros.expand(expression) + def get_active_macros(self) -> List[Macro]: + """ + Gets active macros in the context of the spec file. + + This includes built-in RPM macros, macros loaded from macro files + and macros defined in the spec file itself. + + Returns: + List of `Macro` objects. + """ + self._parser.parse(str(self)) + return Macros.dump() + @contextlib.contextmanager def lines(self) -> Iterator[List[str]]: """ diff --git a/specfile/tags.py b/specfile/tags.py index 7ca7753..d8ea5c3 100644 --- a/specfile/tags.py +++ b/specfile/tags.py @@ -6,71 +6,9 @@ import re from typing import Any, Iterable, List, Optional, SupportsIndex, Union, cast, overload +from specfile.constants import TAG_NAMES, TAGS_WITH_ARG from specfile.sections import Section -# valid tag names as defined in build/parsePreamble.c in RPM source -TAG_NAMES = { - "name", - "version", - "release", - "epoch", - "summary", - "license", - "distribution", - "disturl", - "vendor", - "group", - "packager", - "url", - "vcs", - "source", - "patch", - "nosource", - "nopatch", - "excludearch", - "exclusivearch", - "excludeos", - "exclusiveos", - "icon", - "provides", - "requires", - "recommends", - "suggests", - "supplements", - "enhances", - "prereq", - "conflicts", - "obsoletes", - "prefixes", - "prefix", - "buildroot", - "buildarchitectures", - "buildarch", - "buildconflicts", - "buildprereq", - "buildrequires", - "autoreqprov", - "autoreq", - "autoprov", - "docdir", - "disttag", - "bugurl", - "translationurl", - "upstreamreleases", - "orderwithrequires", - "removepathpostfixes", - "modularitylabel", -} - -# tags that can optionally have an argument (language or qualifier) -TAGS_WITH_ARG = { - "summary", - "group", - "requires", - "prereq", - "orderwithrequires", -} - def get_tag_name_regex(name: str) -> str: """Contructs regex corresponding to the specified tag name.""" @@ -297,6 +235,14 @@ def __repr__(self) -> str: f"'{self._separator}', {comments})" ) + @property + def normalized_name(self) -> str: + """ + Normalized name of the tag. The first character is capitalized + and the rest lowercased. + """ + return self.name.capitalize() + @property def valid(self) -> bool: """Validity of the tag. A tag is valid if it 'survives' the expansion of the spec file.""" diff --git a/specfile/utils.py b/specfile/utils.py index 16295bc..763a8aa 100644 --- a/specfile/utils.py +++ b/specfile/utils.py @@ -10,12 +10,15 @@ import tempfile from typing import Iterator, List +from specfile.constants import ARCH_NAMES from specfile.exceptions import SpecfileException class EVR(collections.abc.Hashable): """Class representing Epoch-Version-Release combination.""" + _regex = r"(?:(\d+):)?([^-]+?)(?:-([^-]+))?" + def __init__(self, *, version: str, release: str = "", epoch: int = 0) -> None: self.epoch = epoch self.version = version @@ -42,7 +45,7 @@ def __str__(self) -> str: @classmethod def from_string(cls, evr: str) -> "EVR": - m = re.match(r"^(?:(\d+):)?([^-]+?)(?:-([^-]+))?$", evr) + m = re.match(f"^{cls._regex}$", evr) if not m: raise SpecfileException("Invalid EVR string.") e, v, r = m.groups() @@ -52,6 +55,8 @@ def from_string(cls, evr: str) -> "EVR": class NEVR(EVR): """Class representing Name-Epoch-Version-Release combination.""" + _regex = r"(.+?)-" + EVR._regex + def __init__( self, *, name: str, version: str, release: str = "", epoch: int = 0 ) -> None: @@ -72,13 +77,49 @@ def __str__(self) -> str: @classmethod def from_string(cls, nevr: str) -> "NEVR": - m = re.match(r"^(.+?)-(?:(\d+):)?([^-]+?)(?:-([^-]+))?$", nevr) + m = re.match(f"^{cls._regex}$", nevr) if not m: raise SpecfileException("Invalid NEVR string.") n, e, v, r = m.groups() return cls(name=n, epoch=int(e) if e else 0, version=v, release=r or "") +class NEVRA(NEVR): + """Class representing Name-Epoch-Version-Release-Arch combination.""" + + _arches_regex = "(" + "|".join(re.escape(a) for a in ARCH_NAMES | {"noarch"}) + ")" + _regex = NEVR._regex + r"\." + _arches_regex + + def __init__( + self, *, name: str, version: str, release: str, arch: str, epoch: int = 0 + ) -> None: + if not re.match(f"^{self._arches_regex}$", arch): + raise SpecfileException("Invalid architecture name.") + self.arch = arch + super().__init__(name=name, epoch=epoch, version=version, release=release) + + def _key(self) -> tuple: + return self.name, self.epoch, self.version, self.release, self.arch + + def __repr__(self) -> str: + return ( + f"NEVRA(name='{self.name}', epoch={self.epoch}, " + f"version='{self.version}', release='{self.release}', " + f"arch='{self.arch}')" + ) + + def __str__(self) -> str: + return super().__str__() + f".{self.arch}" + + @classmethod + def from_string(cls, nevra: str) -> "NEVRA": + m = re.match(f"^{cls._regex}$", nevra) + if not m: + raise SpecfileException("Invalid NEVRA string.") + n, e, v, r, a = m.groups() + return cls(name=n, epoch=int(e) if e else 0, version=v, release=r, arch=a) + + @contextlib.contextmanager def capture_stderr() -> Iterator[List[bytes]]: """ diff --git a/specfile/value_parser.py b/specfile/value_parser.py index 788392e..812716c 100644 --- a/specfile/value_parser.py +++ b/specfile/value_parser.py @@ -5,8 +5,7 @@ import re from abc import ABC from string import Template -from typing import TYPE_CHECKING, List, Optional, Tuple -from typing.re import Pattern +from typing import TYPE_CHECKING, List, Optional, Pattern, Tuple from specfile.exceptions import UnterminatedMacroException from specfile.macros import Macros diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 21304ed..a9af766 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, get_filename_from_location +from specfile.utils import EVR, NEVR, NEVRA, get_filename_from_location @pytest.mark.parametrize( @@ -82,3 +82,43 @@ def test_EVR_from_string(evr, result): ) def test_NEVR_from_string(nevr, result): assert NEVR.from_string(nevr) == result + + +@pytest.mark.parametrize( + "nevra, result", + [ + ( + "package-12.0-1.x86_64", + NEVRA(name="package", version="12.0", release="1", arch="x86_64"), + ), + ( + "package-2:56.8-5.aarch64", + NEVRA(name="package", epoch=2, version="56.8", release="5", arch="aarch64"), + ), + ( + "package-0.8.0-1.fc37.armv6hl", + NEVRA(name="package", version="0.8.0", release="1.fc37", arch="armv6hl"), + ), + ( + "package-0.5.0~rc2-1.el9.noarch", + NEVRA(name="package", version="0.5.0~rc2", release="1.el9", arch="noarch"), + ), + ( + "package-devel-7.3-0.2.rc1.fc38.i686", + NEVRA( + name="package-devel", version="7.3", release="0.2.rc1.fc38", arch="i686" + ), + ), + ( + "package-7.3~rc1^20200701gdeadf00f-12.fc38.riscv", + NEVRA( + name="package", + version="7.3~rc1^20200701gdeadf00f", + release="12.fc38", + arch="riscv", + ), + ), + ], +) +def test_NEVRA_from_string(nevra, result): + assert NEVRA.from_string(nevra) == result