Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-specfile for openSUSE:Factory checked in at 2022-12-15 19:25:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-specfile (Old) and /work/SRC/openSUSE:Factory/.python-specfile.new.1835 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-specfile" Thu Dec 15 19:25:48 2022 rev:4 rq:1043095 version:0.11.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-specfile/python-specfile.changes 2022-12-01 16:59:58.615408352 +0100 +++ /work/SRC/openSUSE:Factory/.python-specfile.new.1835/python-specfile.changes 2022-12-15 19:26:15.996418642 +0100 @@ -1,0 +2,22 @@ +Tue Dec 13 08:20:36 UTC 2022 - David Anes <david.a...@suse.com> + +- Add config.cfg improvements to remove deprecation warnings + * python-specfile-improve-setup-cfg.patch + +- Update to version 0.11.1 + * Tags enclosed in conditional macro expansions are not ignored + anymore. + * Fixed context managers being shared between Specfile instances. 1q + +- Update to version 0.11.0 + * Context managers (Specfile.sections(), Specfile.tags() etc.) can + now be nested and combined together (with one exception - + Specfile.macro_definitions()), and it is also possible to use + tag properties (e.g. Specfile.version, Specfile.license) inside + them. It is also possible to access the data directly, avoiding + the with statement, by using the content property + (e.g. Specfile.tags().content), but be aware that no + modifications done to such data will be preserved. You must use + with to make changes. + +------------------------------------------------------------------- Old: ---- specfile-0.10.0.tar.gz New: ---- python-specfile-improve-setup-cfg.patch specfile-0.11.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-specfile.spec ++++++ --- /var/tmp/diff_new_pack.vBl1UV/_old 2022-12-15 19:26:16.568421896 +0100 +++ /var/tmp/diff_new_pack.vBl1UV/_new 2022-12-15 19:26:16.576421942 +0100 @@ -18,7 +18,7 @@ %define skip_python38 1 Name: python-specfile -Version: 0.10.0 +Version: 0.11.1 Release: 0 Summary: A library for parsing and manipulating RPM spec files License: MIT @@ -38,7 +38,12 @@ Requires: python-rpm Requires: python-typing-extensions +# PATCH-SUSE: some improvements that are still pending upstream +# https://github.com/packit/specfile/pull/162 +Patch0: python-specfile-improve-setup-cfg.patch + BuildArch: noarch + %python_subpackages %description @@ -46,19 +51,22 @@ %prep %autosetup -p1 -n specfile-%{version} +# we use our own package for "rpm" module (see Requires) sed -i '/rpm-py-installer/d' setup.cfg %build %python_build +%check +# Following tests fail: +# * test_update_tag +# * test_macros_reinit +%pytest -k "not (test_update_tag or test_macros_reinit)" + %install %python_install %python_expand %fdupes %{buildroot}%{$python_sitelib} -%check -# test_macros_reinit fails -%pytest -k 'not test_macros_reinit' - %files %{python_files} %doc CHANGELOG.md README.md %license LICENSE ++++++ python-specfile-improve-setup-cfg.patch ++++++ Index: specfile-0.11.1/setup.cfg =================================================================== --- specfile-0.11.1.orig/setup.cfg +++ specfile-0.11.1/setup.cfg @@ -7,7 +7,8 @@ url = https://github.com/packit/specfile author = Red Hat author_email = user-cont-t...@redhat.com license = MIT -license_file = LICENSE +license_files = + LICENSE classifiers = Development Status :: 4 - Beta Environment :: Console ++++++ specfile-0.10.0.tar.gz -> specfile-0.11.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/.packit.yaml new/specfile-0.11.1/.packit.yaml --- old/specfile-0.10.0/.packit.yaml 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/.packit.yaml 2022-12-14 17:34:43.000000000 +0100 @@ -89,6 +89,12 @@ list_on_homepage: True preserve_project: True + - job: pull_from_upstream + trigger: release + dist_git_branches: + - fedora-all + - epel-9 + # downstream automation: - job: koji_build trigger: commit diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/CHANGELOG.md new/specfile-0.11.1/CHANGELOG.md --- old/specfile-0.10.0/CHANGELOG.md 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/CHANGELOG.md 2022-12-14 17:34:43.000000000 +0100 @@ -1,3 +1,12 @@ +# 0.11.1 + +- Tags enclosed in conditional macro expansions are not ignored anymore. (#156) +- Fixed context managers being shared between Specfile instances. (#157) + +# 0.11.0 + +- Context managers (`Specfile.sections()`, `Specfile.tags()` etc.) can now be nested and combined together (with one exception - `Specfile.macro_definitions()`), and it is also possible to use tag properties (e.g. `Specfile.version`, `Specfile.license`) inside them. It is also possible to access the data directly, avoiding the `with` statement, by using the `content` property (e.g. `Specfile.tags().content`), but be aware that no modifications done to such data will be preserved. You must use `with` to make changes. (#153) + # 0.10.0 - Fixed an issue that caused empty lines originally inside changelog entries to appear at the end. (#140) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/PKG-INFO new/specfile-0.11.1/PKG-INFO --- old/specfile-0.10.0/PKG-INFO 2022-11-30 12:28:42.548678900 +0100 +++ new/specfile-0.11.1/PKG-INFO 2022-12-14 17:34:55.061271700 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: specfile -Version: 0.10.0 +Version: 0.11.1 Summary: A library for parsing and manipulating RPM spec files. Home-page: https://github.com/packit/specfile Author: Red Hat diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/fedora/python-specfile.spec new/specfile-0.11.1/fedora/python-specfile.spec --- old/specfile-0.10.0/fedora/python-specfile.spec 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/fedora/python-specfile.spec 2022-12-14 17:34:43.000000000 +0100 @@ -13,7 +13,7 @@ Name: python-specfile -Version: 0.10.0 +Version: 0.11.1 Release: 1%{?dist} Summary: A library for parsing and manipulating RPM spec files @@ -69,6 +69,12 @@ %changelog +* Wed Dec 14 2022 Packit Team <he...@packit.dev> - 0.11.1-1 +- New upstream release 0.11.1 + +* Fri Dec 09 2022 Packit Team <he...@packit.dev> - 0.11.0-1 +- New upstream release 0.11.0 + * Sat Nov 26 2022 Packit Team <he...@packit.dev> - 0.10.0-1 - New upstream release 0.10.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/setup.cfg new/specfile-0.11.1/setup.cfg --- old/specfile-0.10.0/setup.cfg 2022-11-30 12:28:42.548678900 +0100 +++ new/specfile-0.11.1/setup.cfg 2022-12-14 17:34:55.061271700 +0100 @@ -48,6 +48,9 @@ exclude = tests* +[options.package_data] +* = py.typed + [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/context_management.py new/specfile-0.11.1/specfile/context_management.py --- old/specfile-0.10.0/specfile/context_management.py 1970-01-01 01:00:00.000000000 +0100 +++ new/specfile-0.11.1/specfile/context_management.py 2022-12-14 17:34:43.000000000 +0100 @@ -0,0 +1,142 @@ +# Copyright Contributors to the Packit project. +# SPDX-License-Identifier: MIT + +import collections +import contextlib +import io +import os +import pickle +import sys +import tempfile +import types +from typing import Any, Callable, Dict, Generator, List, Optional, overload + + +@contextlib.contextmanager +def capture_stderr() -> Generator[List[bytes], None, None]: + """ + Context manager for capturing output to stderr. A stderr output of anything run + in its context will be captured in the target variable of the with statement. + + Yields: + List of captured lines. + """ + fileno = sys.__stderr__.fileno() + with tempfile.TemporaryFile() as stderr, os.fdopen(os.dup(fileno)) as backup: + sys.stderr.flush() + os.dup2(stderr.fileno(), fileno) + data: List[bytes] = [] + try: + yield data + finally: + sys.stderr.flush() + os.dup2(backup.fileno(), fileno) + stderr.flush() + stderr.seek(0, io.SEEK_SET) + data.extend(stderr.readlines()) + + +class GeneratorContextManager(contextlib._GeneratorContextManager): + """ + Extended contextlib._GeneratorContextManager that provides get() method. + """ + + def __init__(self, function: Callable) -> None: + super().__init__(function, tuple(), {}) + + def __del__(self) -> None: + # make sure the generator is fully consumed, as it is possible + # that neither __enter__() nor content() have been called + collections.deque(self.gen, maxlen=0) + + @property + def content(self) -> Any: + """ + Fully consumes the underlying generator and returns the yielded value. + + Returns: + Value that would normally be the target variable of an associated with statement. + + Raises: + StopIteration if the underlying generator is already exhausted. + """ + result = next(self.gen) + next(self.gen, None) + return result + + +class ContextManager: + """ + Class for decorating generator functions that should act as a context manager. + + Just like with contextlib.contextmanager, the generator returned from the decorated function + must yield exactly one value that will be used as the target variable of the with statement. + If the same function with the same arguments is called again from within previously generated + context, the generator will be ignored and the target variable will be reused. + + Attributes: + function: Decorated generator function. + generators: Mapping of serialized function arguments to generators. + values: Mapping of serialized function arguments to yielded values. + """ + + def __init__(self, function: Callable) -> None: + self.function = function + self.is_bound = False + self.generators: Dict[bytes, Generator[Any, None, None]] = {} + self.values: Dict[bytes, Any] = {} + + @overload + def __get__(self, obj: None, objtype: Optional[type] = None) -> "ContextManager": + pass + + @overload + def __get__(self, obj: object, objtype: Optional[type] = None) -> types.MethodType: + pass + + # implementing __get__() makes the class a non-data descriptor, + # so it can be used as method decorator + def __get__(self, obj, objtype=None): + if obj is None: + return self + self.is_bound = True + return types.MethodType(self, obj) + + def __call__(self, *args: Any, **kwargs: Any) -> GeneratorContextManager: + # serialize the passed arguments + payload = list(args) + sorted(kwargs.items()) + if payload and self.is_bound: + # do not attempt to pickle self/cls + payload[0] = (type(payload[0]), id(payload[0])) + key = pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL) + if ( + key in self.generators + # gi_frame is None only in case generator is exhausted + and self.generators[key].gi_frame is not None # type: ignore[attr-defined] + ): + # generator is suspended, use existing value + def existing_value(): + try: + yield self.values[key] + except KeyError: + # if the generator is being consumed in GeneratorContextManager destructor, + # self.values[key] could have already been deleted + pass + + return GeneratorContextManager(existing_value) + # create the generator + self.generators[key] = self.function(*args, **kwargs) + # first iteration yields the value + self.values[key] = next(self.generators[key]) + + def new_value(): + try: + yield self.values[key] + finally: + # second iteration wraps things up + next(self.generators[key], None) + # the generator is now exhausted and the value is no longer valid + del self.generators[key] + del self.values[key] + + return GeneratorContextManager(new_value) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/macro_definitions.py new/specfile-0.11.1/specfile/macro_definitions.py --- old/specfile-0.10.0/specfile/macro_definitions.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/macro_definitions.py 2022-12-14 17:34:43.000000000 +0100 @@ -37,6 +37,20 @@ macro = "%global" if self.is_global else "%define" return f"{ws[0]}{macro}{ws[1]}{self.name}{ws[2]}{self.body}{ws[3]}" + def get_position(self, container: "MacroDefinitions") -> int: + """ + Gets position of this macro definition in the spec file. + + Args: + container: `MacroDefinitions` instance that contains this macro definition. + + Returns: + Position expressed as line number (starting from 0). + """ + return sum( + len(md.get_raw_data()) for md in container[: container.index(self)] + ) + len(self._preceding_lines) + def get_raw_data(self) -> List[str]: result = self._preceding_lines.copy() ws = self._whitespace diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/macros.py new/specfile-0.11.1/specfile/macros.py --- old/specfile-0.10.0/specfile/macros.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/macros.py 2022-12-14 17:34:43.000000000 +0100 @@ -8,8 +8,8 @@ import rpm +from specfile.context_management import capture_stderr from specfile.exceptions import MacroRemovalException, RPMException -from specfile.utils import capture_stderr MAX_REMOVAL_RETRIES = 20 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/prep.py new/specfile-0.11.1/specfile/prep.py --- old/specfile-0.10.0/specfile/prep.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/prep.py 2022-12-14 17:34:43.000000000 +0100 @@ -8,6 +8,7 @@ from specfile.macro_options import MacroOptions from specfile.sections import Section +from specfile.utils import split_conditional_macro_expansion class PrepMacro(ABC): @@ -330,18 +331,13 @@ Returns: Constructed instance of `Prep` class. """ - # match also macros enclosed in conditionalized macro expansion - # e.g.: %{?with_system_nss:%patch30 -p3 -b .nss_pkcs11_v3} macro_regex = re.compile( - r"(?P<c>%{!?\?\w+:)?.*?" - r"(?P<m>%(setup|patch\d*|autopatch|autosetup))" - r"(?P<d>\s*)" - r"(?P<o>.*?)" - r"(?(c)}|$)" + r"(?P<m>%(setup|patch\d*|autopatch|autosetup))(?P<d>\s*)(?P<o>.*?)$" ) data = [] buffer: List[str] = [] for line in section: + line, prefix, suffix = split_conditional_macro_expansion(line) m = macro_regex.search(line) if m: name, delimiter, option_string = ( @@ -349,7 +345,8 @@ m.group("d"), m.group("o"), ) - prefix, suffix = line[: m.start("m")], line[m.end("o") :] + prefix += line[: m.start("m")] + suffix = line[m.end("o") :] + suffix klass = next( ( klass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/sources.py new/specfile-0.11.1/specfile/sources.py --- old/specfile-0.10.0/specfile/sources.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/sources.py 2022-12-14 17:34:43.000000000 +0100 @@ -38,7 +38,7 @@ @property @abstractmethod - def expanded_location(self) -> str: + def expanded_location(self) -> Optional[str]: """Location of the source after expanding macros.""" ... @@ -56,7 +56,7 @@ @property @abstractmethod - def expanded_filename(self) -> str: + def expanded_filename(self) -> Optional[str]: """Filename of the source after expanding macros.""" ... @@ -86,7 +86,9 @@ def __repr__(self) -> str: tag = repr(self._tag) - return f"TagSource({tag}, {self._number})" + # determine class name dynamically so that inherited classes + # don't have to reimplement __repr__() + return f"{self.__class__.__name__}({tag}, {self._number})" def _extract_number(self) -> Optional[str]: """ @@ -133,7 +135,7 @@ self._tag.value = value @property - def expanded_location(self) -> str: + def expanded_location(self) -> Optional[str]: """Location of the source after expanding macros.""" return self._tag.expanded_value @@ -143,8 +145,10 @@ return get_filename_from_location(self._tag.value) @property - def expanded_filename(self) -> str: + def expanded_filename(self) -> Optional[str]: """Filename of the source after expanding macros.""" + if self._tag.expanded_value is None: + return None return get_filename_from_location(self._tag.expanded_value) @property @@ -154,7 +158,7 @@ class ListSource(Source): - """Class that represents a source backed by a line in a %sourcelist/%patchlist section.""" + """Class that represents a source backed by a line in a %sourcelist section.""" def __init__(self, source: SourcelistEntry, number: int) -> None: """ @@ -172,7 +176,9 @@ def __repr__(self) -> str: source = repr(self._source) - return f"ListSource({source}, {self._number})" + # determine class name dynamically so that inherited classes + # don't have to reimplement __repr__() + return f"{self.__class__.__name__}({source}, {self._number})" @property def number(self) -> int: @@ -212,7 +218,9 @@ class Sources(collections.abc.MutableSequence): """Class that represents a sequence of all sources.""" - PREFIX: str = "Source" + prefix: str = "Source" + tag_class: type = TagSource + list_class: type = ListSource def __init__( self, @@ -330,11 +338,11 @@ result = [] last_number = -1 for i, tag in enumerate(self._tags): - if tag.name.capitalize() == self.PREFIX.capitalize(): + if tag.normalized_name == self.prefix: last_number += 1 - ts = TagSource(tag, last_number) - elif tag.name.capitalize().startswith(self.PREFIX.capitalize()): - ts = TagSource(tag) + ts = self.tag_class(tag, last_number) + elif tag.normalized_name.startswith(self.prefix): + ts = self.tag_class(tag) last_number = ts.number else: continue @@ -356,7 +364,7 @@ ) last_number = result[-1][0].number if result else -1 result.extend( - (ListSource(sl[i], last_number + 1 + i), sl, i) + (self.list_class(sl[i], last_number + 1 + i), sl, i) for sl in self._sourcelists for i in range(len(sl)) ) @@ -399,7 +407,6 @@ Returns: Tuple in the form of (name, separator). """ - prefix = self.PREFIX.capitalize() if number_digits_override is not None: number_digits = number_digits_override else: @@ -408,7 +415,7 @@ suffix = "" else: suffix = f"{number:0{number_digits}}" - name = f"{prefix}{suffix}" + name = f"{self.prefix}{suffix}" diff = len(reference._tag.name) - len(name) if diff >= 0: return name, reference._tag._separator + " " * diff @@ -426,7 +433,6 @@ Returns: Tuple in the form of (index, name, separator). """ - prefix = self.PREFIX.capitalize() if ( self._default_to_implicit_numbering or self._default_source_number_digits == 0 @@ -434,7 +440,7 @@ suffix = "" else: suffix = f"{number:0{self._default_source_number_digits}}" - return len(self._tags) if self._tags else 0, f"{prefix}{suffix}", ": " + return len(self._tags) if self._tags else 0, f"{self.prefix}{suffix}", ": " def _deduplicate_tag_names(self, start: int = 0) -> None: """ @@ -469,7 +475,7 @@ already is a source with the same location. """ if not self._allow_duplicates and location in self: - raise DuplicateSourceException(f"Source '{location}' already exists") + raise DuplicateSourceException(f"{self.prefix} '{location}' already exists") items = self._get_items() if i > len(items): i = len(items) @@ -481,8 +487,8 @@ else: source, container, index = items[i] number = source.number - if isinstance(source, TagSource): - name, separator = self._get_tag_format(source, number) + if isinstance(source, self.tag_class): + name, separator = self._get_tag_format(cast(TagSource, source), number) container.insert( index, Tag(name, location, self._expand(location), separator, Comments()), @@ -518,7 +524,7 @@ already is a source with the same location. """ if not self._allow_duplicates and location in self: - raise DuplicateSourceException(f"Source '{location}' already exists") + raise DuplicateSourceException(f"{self.prefix} '{location}' already exists") tags = self._get_tags() if tags: # find the nearest source tag @@ -580,10 +586,24 @@ return len([s for s in list(zip(*items))[0] if s.location == location]) +class Patch(Source): + """Class that represents a patch.""" + + +class TagPatch(TagSource, Patch): + """Class that represents a patch backed by a spec file tag.""" + + +class ListPatch(ListSource, Patch): + """Class that represents a patch backed by a line in a %patchlist section.""" + + class Patches(Sources): """Class that represents a sequence of all patches.""" - PREFIX: str = "Patch" + prefix: str = "Patch" + tag_class: type = TagPatch + list_class: type = ListPatch def _get_initial_tag_setup(self, number: int = 0) -> Tuple[int, str, str]: """ @@ -599,9 +619,9 @@ """ try: index, source = [ - (i, TagSource(t)) + (i, Sources.tag_class(t)) for i, t in enumerate(self._tags) - if t.name.capitalize().startswith("Source") + if t.normalized_name.startswith(Sources.prefix) ][-1] except IndexError: return super()._get_initial_tag_setup(number) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/spec_parser.py new/specfile-0.11.1/specfile/spec_parser.py --- old/specfile-0.10.0/specfile/spec_parser.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/spec_parser.py 2022-12-14 17:34:43.000000000 +0100 @@ -11,11 +11,12 @@ import rpm +from specfile.context_management import capture_stderr from specfile.exceptions import RPMException from specfile.macros import Macros from specfile.sections import Section from specfile.tags import Tags -from specfile.utils import capture_stderr, get_filename_from_location +from specfile.utils import get_filename_from_location from specfile.value_parser import ConditionalMacroExpansion, ShellExpansion, ValueParser logger = logging.getLogger(__name__) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/specfile.py new/specfile-0.11.1/specfile/specfile.py --- old/specfile-0.10.0/specfile/specfile.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/specfile.py 2022-12-14 17:34:43.000000000 +0100 @@ -1,18 +1,18 @@ # Copyright Contributors to the Packit project. # SPDX-License-Identifier: MIT -import contextlib import datetime import re import subprocess import types from dataclasses import dataclass from pathlib import Path -from typing import Iterator, List, Optional, Tuple, Type, Union +from typing import Generator, List, Optional, Tuple, Type, Union import rpm from specfile.changelog import Changelog, ChangelogEntry +from specfile.context_management import ContextManager from specfile.exceptions import SourceNumberException, SpecfileException from specfile.macro_definitions import MacroDefinition, MacroDefinitions from specfile.macros import Macro, Macros @@ -148,8 +148,8 @@ self._parser.parse(str(self)) return Macros.dump() - @contextlib.contextmanager - def lines(self) -> Iterator[List[str]]: + @ContextManager + def lines(self) -> Generator[List[str], None, None]: """ Context manager for accessing spec file lines. @@ -163,8 +163,8 @@ if self.autosave: self.save() - @contextlib.contextmanager - def macro_definitions(self) -> Iterator[MacroDefinitions]: + @ContextManager + def macro_definitions(self) -> Generator[MacroDefinitions, None, None]: """ Context manager for accessing macro definitions. @@ -178,8 +178,8 @@ finally: lines[:] = macro_definitions.get_raw_data() - @contextlib.contextmanager - def sections(self) -> Iterator[Sections]: + @ContextManager + def sections(self) -> Generator[Sections, None, None]: """ Context manager for accessing spec file sections. @@ -200,8 +200,10 @@ return None return Sections.parse(self._parser.spec.parsed.splitlines()) - @contextlib.contextmanager - def tags(self, section: Union[str, Section] = "package") -> Iterator[Tags]: + @ContextManager + def tags( + self, section: Union[str, Section] = "package" + ) -> Generator[Tags, None, None]: """ Context manager for accessing tags in a specified section. @@ -212,26 +214,21 @@ Yields: Tags in the section as `Tags` object. """ - if isinstance(section, Section): - raw_section = section - parsed_section = getattr(self.parsed_sections, section.name, None) + with self.sections() as sections: + if isinstance(section, Section): + raw_section = section + parsed_section = getattr(self.parsed_sections, section.name, None) + else: + raw_section = getattr(sections, section) + parsed_section = getattr(self.parsed_sections, section, None) tags = Tags.parse(raw_section, parsed_section) try: yield tags finally: raw_section.data = tags.get_raw_section_data() - else: - with self.sections() as sections: - raw_section = getattr(sections, section) - parsed_section = getattr(self.parsed_sections, section, None) - tags = Tags.parse(raw_section, parsed_section) - try: - yield tags - finally: - raw_section.data = tags.get_raw_section_data() - @contextlib.contextmanager - def changelog(self) -> Iterator[Optional[Changelog]]: + @ContextManager + def changelog(self) -> Generator[Optional[Changelog], None, None]: """ Context manager for accessing changelog. @@ -250,8 +247,8 @@ finally: section.data = changelog.get_raw_section_data() - @contextlib.contextmanager - def prep(self) -> Iterator[Optional[Prep]]: + @ContextManager + def prep(self) -> Generator[Optional[Prep], None, None]: """ Context manager for accessing %prep section. @@ -270,13 +267,13 @@ finally: section.data = prep.get_raw_section_data() - @contextlib.contextmanager + @ContextManager def sources( self, allow_duplicates: bool = False, default_to_implicit_numbering: bool = False, default_source_number_digits: int = 1, - ) -> Iterator[Sources]: + ) -> Generator[Sources, None, None]: """ Context manager for accessing sources. @@ -288,7 +285,7 @@ Yields: Spec file sources as `Sources` object. """ - with self.sections() as sections, self.tags(sections.package) as tags: + with self.sections() as sections, self.tags() as tags: sourcelists = [ (s, Sourcelist.parse(s, context=self)) for s in sections @@ -307,13 +304,13 @@ for section, sourcelist in sourcelists: section.data = sourcelist.get_raw_section_data() - @contextlib.contextmanager + @ContextManager def patches( self, allow_duplicates: bool = False, default_to_implicit_numbering: bool = False, default_source_number_digits: int = 1, - ) -> Iterator[Patches]: + ) -> Generator[Patches, None, None]: """ Context manager for accessing patches. @@ -325,7 +322,7 @@ Yields: Spec file patches as `Patches` object. """ - with self.sections() as sections, self.tags(sections.package) as tags: + with self.sections() as sections, self.tags() as tags: patchlists = [ (s, Sourcelist.parse(s, context=self)) for s in sections @@ -530,7 +527,7 @@ @property def expanded_release(self) -> str: """Release string without the dist suffix with macros expanded.""" - return self.expand(self.release) + return self.expand(self.release, extra_macros=[("dist", "")]) def set_version_and_release(self, version: str, release: str = "1") -> None: """ @@ -581,7 +578,11 @@ patches[index].comments.extend(comment.splitlines()) def update_value( - self, value: str, requested_value: str, protected_entities: Optional[str] = None + self, + value: str, + requested_value: str, + position: int, + protected_entities: Optional[str] = None, ) -> str: """ Updates a value from within the context of the spec file with a new value, @@ -591,6 +592,7 @@ Args: value: Value to update. requested_value: Requested new value. + position: Position (line number) of the value in the spec file. protected_entities: Regular expression specifying protected tags and macro definitions, ensuring their values won't be updated. @@ -600,8 +602,10 @@ @dataclass class Entity: + name: str value: str type: Type + position: int locked: bool = False updated: bool = False @@ -611,32 +615,38 @@ re.IGNORECASE, ) # collect modifiable entities - entities = {} + entities = [] with self.macro_definitions() as macro_definitions: - entities.update( - { - md.name: Entity(md.body, type(md)) + entities.extend( + [ + Entity( + md.name, md.body, type(md), md.get_position(macro_definitions) + ) for md in macro_definitions if not protected_regex.match(md.name) and not md.name.endswith(")") # skip macro definitions with options - } + ] ) - # order matters here - if there is a macro definition redefining a tag, - # we want to update the tag, not the macro definition with self.tags() as tags: - entities.update( - { - t.name.lower(): Entity(t.value, type(t)) + entities.extend( + [ + Entity(t.name.lower(), t.value, type(t), t.get_position(tags)) for t in tags if not protected_regex.match(t.name) - } + ] ) - # tags can be referenced as %{tag} or %{TAG} - entities.update({k.upper(): v for k, v in entities.items() if v.type == Tag}) + entities.sort(key=lambda e: e.position) - def update(value, requested_value): + def update(value, requested_value, position): + modifiable_entities = {e.name for e in entities if e.position < position} + # tags can be referenced as %{tag} or %{TAG} + modifiable_entities.update( + e.name.upper() + for e in entities + if e.position < position and e.type == Tag + ) regex, template = ValueParser.construct_regex( - value, entities.keys(), context=self + value, modifiable_entities, context=self ) m = regex.match(requested_value) if m: @@ -644,33 +654,41 @@ for grp, val in d.items(): if grp.startswith(SUBSTITUTION_GROUP_PREFIX): continue - if entities[grp].locked: + # find the closest matching entity + entity = [ + e + for e in entities + if e.position < position + and ( + e.name == grp + and e.type == MacroDefinition + or e.name == grp.lower() + and e.type == Tag + ) + ][-1] + if entity.locked: # avoid infinite recursion return requested_value - entities[grp].locked = True + entity.locked = True try: - entities[grp].value = update(entities[grp].value, val) + entity.value = update(entity.value, val, entity.position) finally: - entities[grp].locked = False - entities[grp].updated = True + entity.locked = False + entity.updated = True return template.substitute(d) # no match, simply return the requested value return requested_value - result = update(value, requested_value) + result = update(value, requested_value, position) # synchronize back any changes with self.macro_definitions() as macro_definitions: - for n, v in [ - (n, v) - for n, v in entities.items() - if v.updated and v.type == MacroDefinition + for entity in [ + e for e in entities if e.updated and e.type == MacroDefinition ]: - getattr(macro_definitions, n).body = v.value + getattr(macro_definitions, entity.name).body = entity.value with self.tags() as tags: - for n, v in [ - (n, v) for n, v in entities.items() if v.updated and v.type == Tag - ]: - getattr(tags, n).value = v.value + for entity in [e for e in entities if e.updated and e.type == Tag]: + getattr(tags, entity.name).value = entity.value return result def update_tag( @@ -688,11 +706,13 @@ ensuring their values won't be updated. """ with self.tags() as tags: - original_value = getattr(tags, name).value + tag = getattr(tags, name) + original_value = tag.value + position = tag.get_position(tags) # we can't use update_value() within the context manager, because any changes # made by it to tags or macro definitions would be thrown away updated_value = self.update_value( - original_value, value, protected_entities=protected_entities + original_value, value, position, protected_entities=protected_entities ) with self.tags() as tags: getattr(tags, name).value = updated_value diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/tags.py new/specfile-0.11.1/specfile/tags.py --- old/specfile-0.10.0/specfile/tags.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/tags.py 2022-12-14 17:34:43.000000000 +0100 @@ -8,6 +8,7 @@ from specfile.constants import TAG_NAMES, TAGS_WITH_ARG from specfile.sections import Section +from specfile.utils import split_conditional_macro_expansion def get_tag_name_regex(name: str) -> str: @@ -187,9 +188,11 @@ self, name: str, value: str, - expanded_value: str, + expanded_value: Optional[str], separator: str, comments: Comments, + prefix: Optional[str] = None, + suffix: Optional[str] = None, ) -> None: """ Constructs a `Tag` object. @@ -202,6 +205,8 @@ Separator between name and literal value (colon usually surrounded by some amount of whitespace). comments: List of comments associated with the tag. + prefix: Characters preceding the tag on a line. + suffix: Characters following the tag on a line. Returns: Constructed instance of `Tag` class. @@ -216,6 +221,8 @@ self._expanded_value = expanded_value self._separator = separator self.comments = comments.copy() + self._prefix = prefix or "" + self._suffix = suffix or "" def __eq__(self, other: object) -> bool: if not isinstance(other, Tag): @@ -226,13 +233,16 @@ and self._expanded_value == other._expanded_value and self._separator == other._separator and self.comments == other.comments + and self._prefix == other._prefix + and self._suffix == other._suffix ) def __repr__(self) -> str: comments = repr(self.comments) + expanded_value = repr(self._expanded_value) return ( - f"Tag('{self.name}', '{self.value}', '{self._expanded_value}', " - f"'{self._separator}', {comments})" + f"Tag('{self.name}', '{self.value}', {expanded_value}, " + f"'{self._separator}', {comments}, '{self._prefix}', '{self._suffix}')" ) @property @@ -249,10 +259,25 @@ return self._expanded_value is not None @property - def expanded_value(self) -> str: + def expanded_value(self) -> Optional[str]: """Value of the tag after expanding macros and evaluating all conditions.""" return self._expanded_value + def get_position(self, container: "Tags") -> int: + """ + Gets position of this tag in a section. + + Args: + container: `Tags` instance that contains this tag. + + Returns: + Position expressed as line number (starting from 0). + """ + return sum( + len(t.comments.get_raw_data()) + 1 + for t in container[: container.index(self)] + ) + len(self.comments.get_raw_data()) + class Tags(collections.UserList): """ @@ -426,6 +451,7 @@ data = [] buffer: List[str] = [] for line in raw_section: + line, prefix, suffix = split_conditional_macro_expansion(line) # 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: @@ -447,6 +473,8 @@ expanded_value, m.group("s"), Comments.parse(buffer), + prefix, + suffix, ) ) buffer = [] @@ -464,6 +492,8 @@ result = [] for tag in self.data: result.extend(tag.comments.get_raw_data()) - result.append(f"{tag.name}{tag._separator}{tag.value}") + result.append( + f"{tag._prefix}{tag.name}{tag._separator}{tag.value}{tag._suffix}" + ) result.extend(self._remainder) return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/utils.py new/specfile-0.11.1/specfile/utils.py --- old/specfile-0.10.0/specfile/utils.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/utils.py 2022-12-14 17:34:43.000000000 +0100 @@ -2,16 +2,12 @@ # SPDX-License-Identifier: MIT import collections -import contextlib -import io -import os import re -import sys -import tempfile -from typing import Iterator, List +from typing import Tuple from specfile.constants import ARCH_NAMES -from specfile.exceptions import SpecfileException +from specfile.exceptions import SpecfileException, UnterminatedMacroException +from specfile.value_parser import ConditionalMacroExpansion, ValueParser class EVR(collections.abc.Hashable): @@ -120,30 +116,6 @@ 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]]: - """ - Context manager for capturing output to stderr. A stderr output of anything run - in its context will be captured in the target variable of the with statement. - - Yields: - List of captured lines. - """ - fileno = sys.__stderr__.fileno() - with tempfile.TemporaryFile() as stderr, os.fdopen(os.dup(fileno)) as backup: - sys.stderr.flush() - os.dup2(stderr.fileno(), fileno) - data: List[bytes] = [] - try: - yield data - finally: - sys.stderr.flush() - os.dup2(backup.fileno(), fileno) - stderr.flush() - stderr.seek(0, io.SEEK_SET) - data.extend(stderr.readlines()) - - def get_filename_from_location(location: str) -> str: """ Extracts filename from given source location. @@ -160,3 +132,27 @@ if slash < 0: return location return location[slash + 1 :].split("=")[-1] + + +def split_conditional_macro_expansion(value: str) -> Tuple[str, str, str]: + """ + Splits conditional macro expansion into its body and prefix and suffix of it. + If the passed string isn't a conditional macro expansion, returns it as it is. + + Args: + value: String to be split. + + Returns: + Tuple of body, prefix, suffix. Prefix and suffix will be empty if the passed string + isn't a conditional macro expansion. + """ + try: + nodes = ValueParser.parse(value) + except UnterminatedMacroException: + return value, "", "" + if len(nodes) != 1: + return value, "", "" + node = nodes[0] + if not isinstance(node, ConditionalMacroExpansion): + return value, "", "" + return "".join(str(n) for n in node.body), f"%{{{node.prefix}{node.name}:", "}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile/value_parser.py new/specfile-0.11.1/specfile/value_parser.py --- old/specfile-0.10.0/specfile/value_parser.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/specfile/value_parser.py 2022-12-14 17:34:43.000000000 +0100 @@ -5,7 +5,7 @@ import re from abc import ABC from string import Template -from typing import TYPE_CHECKING, List, Optional, Pattern, Tuple +from typing import TYPE_CHECKING, List, Optional, Pattern, Set, Tuple from specfile.exceptions import UnterminatedMacroException from specfile.macros import Macros @@ -254,7 +254,7 @@ def construct_regex( cls, value: str, - modifiable_entities: List[str], + modifiable_entities: Set[str], context: Optional["Specfile"] = None, ) -> Tuple[Pattern, Template]: """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile.egg-info/PKG-INFO new/specfile-0.11.1/specfile.egg-info/PKG-INFO --- old/specfile-0.10.0/specfile.egg-info/PKG-INFO 2022-11-30 12:28:42.000000000 +0100 +++ new/specfile-0.11.1/specfile.egg-info/PKG-INFO 2022-12-14 17:34:54.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: specfile -Version: 0.10.0 +Version: 0.11.1 Summary: A library for parsing and manipulating RPM spec files. Home-page: https://github.com/packit/specfile Author: Red Hat diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/specfile.egg-info/SOURCES.txt new/specfile-0.11.1/specfile.egg-info/SOURCES.txt --- old/specfile-0.10.0/specfile.egg-info/SOURCES.txt 2022-11-30 12:28:42.000000000 +0100 +++ new/specfile-0.11.1/specfile.egg-info/SOURCES.txt 2022-12-14 17:34:55.000000000 +0100 @@ -35,11 +35,13 @@ specfile/__init__.py specfile/changelog.py specfile/constants.py +specfile/context_management.py specfile/exceptions.py specfile/macro_definitions.py specfile/macro_options.py specfile/macros.py specfile/prep.py +specfile/py.typed specfile/sections.py specfile/sourcelist.py specfile/sources.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/tests/data/spec_macros/test.spec new/specfile-0.11.1/tests/data/spec_macros/test.spec --- old/specfile-0.10.0/tests/data/spec_macros/test.spec 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/tests/data/spec_macros/test.spec 2022-12-14 17:34:43.000000000 +0100 @@ -3,11 +3,12 @@ %global patchver 2 %global prever rc2 %global package_version %{majorver}.%{minorver}.%{patchver} +%global release 1%{?dist} Name: test Version: %{package_version}%{?prever:~%{prever}} -Release: 1%{?dist} +Release: %{release} Summary: Test package License: MIT diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/tests/integration/test_specfile.py new/specfile-0.11.1/tests/integration/test_specfile.py --- old/specfile-0.10.0/tests/integration/test_specfile.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/tests/integration/test_specfile.py 2022-12-14 17:34:43.000000000 +0100 @@ -311,6 +311,10 @@ assert md.prever.body == "alpha1" assert md.package_version.body == "4.0" assert spec.version == "5.3.3" + spec.update_tag("Release", "2%{?dist}") + assert spec.raw_release == "%{release}" + with spec.macro_definitions() as md: + assert md.release.body == "2%{?dist}" spec.update_tag( "Source0", "https://example.com/archived_releases/test/v6.0.0/test-v6.0.0.tar.xz", @@ -387,3 +391,25 @@ spec = Specfile(spec_shell_expansions) assert spec.expanded_version == "1035.4200" assert "C.UTF-8" in spec.expand("%numeric_locale") + + +def test_context_management(spec_autosetup, spec_traditional): + spec = Specfile(spec_autosetup) + with spec.tags() as tags: + tags.license.value = "BSD" + assert spec.license == "BSD" + spec.license = "BSD-3-Clause" + tags.patch0.value = "first_patch.patch" + with spec.patches() as patches: + assert patches[0].location == "first_patch.patch" + patches[0].location = "patch_0.patch" + assert spec.license == "BSD-3-Clause" + with spec.patches() as patches: + assert patches[0].location == "patch_0.patch" + spec1 = Specfile(spec_autosetup) + spec2 = Specfile(spec_traditional) + with spec1.sections() as sections1, spec2.sections() as sections2: + assert sections1 is not sections2 + with spec1.tags() as tags1, spec2.tags() as tags2: + assert tags1 is not tags2 + assert tags1 == tags2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/tests/unit/test_sources.py new/specfile-0.11.1/tests/unit/test_sources.py --- old/specfile-0.10.0/tests/unit/test_sources.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/tests/unit/test_sources.py 2022-12-14 17:34:43.000000000 +0100 @@ -246,7 +246,7 @@ for sl in sourcelists ], ) - if location in [v for t, v in tags if t.startswith(Sources.PREFIX)] + [ + if location in [v for t, v in tags if t.startswith(Sources.prefix)] + [ s for sl in sourcelists for s in sl ]: with pytest.raises(SpecfileException): @@ -333,7 +333,7 @@ ) def test_sources_insert_numbered(tags, number, location, index): sources = Sources(Tags([Tag(t, v, v, ": ", Comments()) for t, v in tags]), []) - if location in [v for t, v in tags if t.startswith(Sources.PREFIX)]: + if location in [v for t, v in tags if t.startswith(Sources.prefix)]: with pytest.raises(SpecfileException): sources.insert_numbered(number, location) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/specfile-0.10.0/tests/unit/test_tags.py new/specfile-0.11.1/tests/unit/test_tags.py --- old/specfile-0.10.0/tests/unit/test_tags.py 2022-11-30 12:28:29.000000000 +0100 +++ new/specfile-0.11.1/tests/unit/test_tags.py 2022-12-14 17:34:43.000000000 +0100 @@ -43,6 +43,8 @@ "", "Requires: make", "Requires(post): bash", + "", + "%{?fedora:Suggests: diffutils}", ], ), Section( @@ -64,6 +66,8 @@ "", "Requires: make", "Requires(post): bash", + "", + "Suggests: diffutils", ], ), ) @@ -80,7 +84,9 @@ assert not tags.epoch.valid assert tags.requires.value == "make" assert "requires(post)" in tags - assert tags[-1].name == "Requires(post)" + assert tags[-2].name == "Requires(post)" + assert tags[-1].name == "Suggests" + assert tags.suggests.value == "diffutils" def test_get_raw_section_data(): @@ -112,6 +118,15 @@ "Requires", "make", "make", ": ", Comments([], ["%endif", ""]) ), Tag("Requires(post)", "bash", "bash", ": ", Comments()), + Tag( + "Suggests", + "diffutils", + "diffutils", + ": ", + Comments([], [""]), + "%{?fedora:", + "}", + ), ], [], ) @@ -132,6 +147,8 @@ "", "Requires: make", "Requires(post): bash", + "", + "%{?fedora:Suggests: diffutils}", ]