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 <[email protected]>
+
+- 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 = [email protected]
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 <[email protected]> - 0.11.1-1
+- New upstream release 0.11.1
+
+* Fri Dec 09 2022 Packit Team <[email protected]> - 0.11.0-1
+- New upstream release 0.11.0
+
* Sat Nov 26 2022 Packit Team <[email protected]> - 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
+
+
[email protected]
+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)
[email protected]
-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}",
]